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_editor::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, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher,
24 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, separator, 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 separator!("a.txt"),
1680 separator!("b.txt"),
1681 separator!("subdir2"),
1682 separator!("subdir2/f.txt"),
1683 separator!("subdir2/g.txt"),
1684 separator!("subdir2/h.txt"),
1685 separator!("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 separator!("a.txt"),
1713 separator!("b.txt"),
1714 separator!("subdir2"),
1715 separator!("subdir2/f.txt"),
1716 separator!("subdir2/g.txt"),
1717 separator!("subdir2/h.txt"),
1718 separator!("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 separator!("a.txt"),
1810 separator!("b.txt"),
1811 separator!("subdir2"),
1812 separator!("subdir2/f.txt"),
1813 separator!("subdir2/g.txt"),
1814 separator!("subdir2/h.txt"),
1815 separator!("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 email: None,
1880 }),
1881 project_id: project_a_id,
1882 worktree_root_names: vec!["a".to_string()],
1883 }]
1884 );
1885
1886 let project_b_id = active_call_b
1887 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1888 .await
1889 .unwrap();
1890 executor.run_until_parked();
1891 assert_eq!(
1892 mem::take(&mut *events_a.borrow_mut()),
1893 vec![room::Event::RemoteProjectShared {
1894 owner: Arc::new(User {
1895 id: client_b.user_id().unwrap(),
1896 github_login: "user_b".to_string(),
1897 avatar_uri: "avatar_b".into(),
1898 name: None,
1899 email: None,
1900 }),
1901 project_id: project_b_id,
1902 worktree_root_names: vec!["b".to_string()]
1903 }]
1904 );
1905 assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]);
1906
1907 // Sharing a project twice is idempotent.
1908 let project_b_id_2 = active_call_b
1909 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1910 .await
1911 .unwrap();
1912 assert_eq!(project_b_id_2, project_b_id);
1913 executor.run_until_parked();
1914 assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]);
1915 assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]);
1916
1917 // Unsharing a project should dispatch the RemoteProjectUnshared event.
1918 active_call_a
1919 .update(cx_a, |call, cx| call.hang_up(cx))
1920 .await
1921 .unwrap();
1922 executor.run_until_parked();
1923
1924 assert_eq!(
1925 mem::take(&mut *events_a.borrow_mut()),
1926 vec![room::Event::RoomLeft { channel_id: None }]
1927 );
1928 assert_eq!(
1929 mem::take(&mut *events_b.borrow_mut()),
1930 vec![room::Event::RemoteProjectUnshared {
1931 project_id: project_a_id,
1932 }]
1933 );
1934}
1935
1936fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>> {
1937 let events = Rc::new(RefCell::new(Vec::new()));
1938 let active_call = cx.read(ActiveCall::global);
1939 cx.update({
1940 let events = events.clone();
1941 |cx| {
1942 cx.subscribe(&active_call, move |_, event, _| {
1943 events.borrow_mut().push(event.clone())
1944 })
1945 .detach()
1946 }
1947 });
1948 events
1949}
1950
1951#[gpui::test]
1952async fn test_mute_deafen(
1953 executor: BackgroundExecutor,
1954 cx_a: &mut TestAppContext,
1955 cx_b: &mut TestAppContext,
1956 cx_c: &mut TestAppContext,
1957) {
1958 let mut server = TestServer::start(executor.clone()).await;
1959 let client_a = server.create_client(cx_a, "user_a").await;
1960 let client_b = server.create_client(cx_b, "user_b").await;
1961 let client_c = server.create_client(cx_c, "user_c").await;
1962
1963 server
1964 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1965 .await;
1966
1967 let active_call_a = cx_a.read(ActiveCall::global);
1968 let active_call_b = cx_b.read(ActiveCall::global);
1969 let active_call_c = cx_c.read(ActiveCall::global);
1970
1971 // User A calls user B, B answers.
1972 active_call_a
1973 .update(cx_a, |call, cx| {
1974 call.invite(client_b.user_id().unwrap(), None, cx)
1975 })
1976 .await
1977 .unwrap();
1978 executor.run_until_parked();
1979 active_call_b
1980 .update(cx_b, |call, cx| call.accept_incoming(cx))
1981 .await
1982 .unwrap();
1983 executor.run_until_parked();
1984
1985 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
1986 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
1987
1988 room_a.read_with(cx_a, |room, _| assert!(!room.is_muted()));
1989 room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
1990
1991 // Users A and B are both unmuted.
1992 assert_eq!(
1993 participant_audio_state(&room_a, cx_a),
1994 &[ParticipantAudioState {
1995 user_id: client_b.user_id().unwrap(),
1996 is_muted: false,
1997 audio_tracks_playing: vec![true],
1998 }]
1999 );
2000 assert_eq!(
2001 participant_audio_state(&room_b, cx_b),
2002 &[ParticipantAudioState {
2003 user_id: client_a.user_id().unwrap(),
2004 is_muted: false,
2005 audio_tracks_playing: vec![true],
2006 }]
2007 );
2008
2009 // User A mutes
2010 room_a.update(cx_a, |room, cx| room.toggle_mute(cx));
2011 executor.run_until_parked();
2012
2013 // User A hears user B, but B doesn't hear A.
2014 room_a.read_with(cx_a, |room, _| assert!(room.is_muted()));
2015 room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
2016 assert_eq!(
2017 participant_audio_state(&room_a, cx_a),
2018 &[ParticipantAudioState {
2019 user_id: client_b.user_id().unwrap(),
2020 is_muted: false,
2021 audio_tracks_playing: vec![true],
2022 }]
2023 );
2024 assert_eq!(
2025 participant_audio_state(&room_b, cx_b),
2026 &[ParticipantAudioState {
2027 user_id: client_a.user_id().unwrap(),
2028 is_muted: true,
2029 audio_tracks_playing: vec![true],
2030 }]
2031 );
2032
2033 // User A deafens
2034 room_a.update(cx_a, |room, cx| room.toggle_deafen(cx));
2035 executor.run_until_parked();
2036
2037 // User A does not hear user B.
2038 room_a.read_with(cx_a, |room, _| assert!(room.is_muted()));
2039 room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
2040 assert_eq!(
2041 participant_audio_state(&room_a, cx_a),
2042 &[ParticipantAudioState {
2043 user_id: client_b.user_id().unwrap(),
2044 is_muted: false,
2045 audio_tracks_playing: vec![false],
2046 }]
2047 );
2048 assert_eq!(
2049 participant_audio_state(&room_b, cx_b),
2050 &[ParticipantAudioState {
2051 user_id: client_a.user_id().unwrap(),
2052 is_muted: true,
2053 audio_tracks_playing: vec![true],
2054 }]
2055 );
2056
2057 // User B calls user C, C joins.
2058 active_call_b
2059 .update(cx_b, |call, cx| {
2060 call.invite(client_c.user_id().unwrap(), None, cx)
2061 })
2062 .await
2063 .unwrap();
2064 executor.run_until_parked();
2065 active_call_c
2066 .update(cx_c, |call, cx| call.accept_incoming(cx))
2067 .await
2068 .unwrap();
2069 executor.run_until_parked();
2070
2071 // User A does not hear users B or C.
2072 assert_eq!(
2073 participant_audio_state(&room_a, cx_a),
2074 &[
2075 ParticipantAudioState {
2076 user_id: client_b.user_id().unwrap(),
2077 is_muted: false,
2078 audio_tracks_playing: vec![false],
2079 },
2080 ParticipantAudioState {
2081 user_id: client_c.user_id().unwrap(),
2082 is_muted: false,
2083 audio_tracks_playing: vec![false],
2084 }
2085 ]
2086 );
2087 assert_eq!(
2088 participant_audio_state(&room_b, cx_b),
2089 &[
2090 ParticipantAudioState {
2091 user_id: client_a.user_id().unwrap(),
2092 is_muted: true,
2093 audio_tracks_playing: vec![true],
2094 },
2095 ParticipantAudioState {
2096 user_id: client_c.user_id().unwrap(),
2097 is_muted: false,
2098 audio_tracks_playing: vec![true],
2099 }
2100 ]
2101 );
2102
2103 #[derive(PartialEq, Eq, Debug)]
2104 struct ParticipantAudioState {
2105 user_id: u64,
2106 is_muted: bool,
2107 audio_tracks_playing: Vec<bool>,
2108 }
2109
2110 fn participant_audio_state(
2111 room: &Entity<Room>,
2112 cx: &TestAppContext,
2113 ) -> Vec<ParticipantAudioState> {
2114 room.read_with(cx, |room, _| {
2115 room.remote_participants()
2116 .iter()
2117 .map(|(user_id, participant)| ParticipantAudioState {
2118 user_id: *user_id,
2119 is_muted: participant.muted,
2120 audio_tracks_playing: participant
2121 .audio_tracks
2122 .values()
2123 .map(|(track, _)| track.enabled())
2124 .collect(),
2125 })
2126 .collect::<Vec<_>>()
2127 })
2128 }
2129}
2130
2131#[gpui::test(iterations = 10)]
2132async fn test_room_location(
2133 executor: BackgroundExecutor,
2134 cx_a: &mut TestAppContext,
2135 cx_b: &mut TestAppContext,
2136) {
2137 let mut server = TestServer::start(executor.clone()).await;
2138 let client_a = server.create_client(cx_a, "user_a").await;
2139 let client_b = server.create_client(cx_b, "user_b").await;
2140 client_a.fs().insert_tree("/a", json!({})).await;
2141 client_b.fs().insert_tree("/b", json!({})).await;
2142
2143 let active_call_a = cx_a.read(ActiveCall::global);
2144 let active_call_b = cx_b.read(ActiveCall::global);
2145
2146 let a_notified = Rc::new(Cell::new(false));
2147 cx_a.update({
2148 let notified = a_notified.clone();
2149 |cx| {
2150 cx.observe(&active_call_a, move |_, _| notified.set(true))
2151 .detach()
2152 }
2153 });
2154
2155 let b_notified = Rc::new(Cell::new(false));
2156 cx_b.update({
2157 let b_notified = b_notified.clone();
2158 |cx| {
2159 cx.observe(&active_call_b, move |_, _| b_notified.set(true))
2160 .detach()
2161 }
2162 });
2163
2164 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
2165 active_call_a
2166 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2167 .await
2168 .unwrap();
2169 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
2170
2171 server
2172 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2173 .await;
2174
2175 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
2176
2177 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
2178 executor.run_until_parked();
2179 assert!(a_notified.take());
2180 assert_eq!(
2181 participant_locations(&room_a, cx_a),
2182 vec![("user_b".to_string(), ParticipantLocation::External)]
2183 );
2184 assert!(b_notified.take());
2185 assert_eq!(
2186 participant_locations(&room_b, cx_b),
2187 vec![("user_a".to_string(), ParticipantLocation::UnsharedProject)]
2188 );
2189
2190 let project_a_id = active_call_a
2191 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2192 .await
2193 .unwrap();
2194 executor.run_until_parked();
2195 assert!(a_notified.take());
2196 assert_eq!(
2197 participant_locations(&room_a, cx_a),
2198 vec![("user_b".to_string(), ParticipantLocation::External)]
2199 );
2200 assert!(b_notified.take());
2201 assert_eq!(
2202 participant_locations(&room_b, cx_b),
2203 vec![(
2204 "user_a".to_string(),
2205 ParticipantLocation::SharedProject {
2206 project_id: project_a_id
2207 }
2208 )]
2209 );
2210
2211 let project_b_id = active_call_b
2212 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
2213 .await
2214 .unwrap();
2215 executor.run_until_parked();
2216 assert!(a_notified.take());
2217 assert_eq!(
2218 participant_locations(&room_a, cx_a),
2219 vec![("user_b".to_string(), ParticipantLocation::External)]
2220 );
2221 assert!(b_notified.take());
2222 assert_eq!(
2223 participant_locations(&room_b, cx_b),
2224 vec![(
2225 "user_a".to_string(),
2226 ParticipantLocation::SharedProject {
2227 project_id: project_a_id
2228 }
2229 )]
2230 );
2231
2232 active_call_b
2233 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2234 .await
2235 .unwrap();
2236 executor.run_until_parked();
2237 assert!(a_notified.take());
2238 assert_eq!(
2239 participant_locations(&room_a, cx_a),
2240 vec![(
2241 "user_b".to_string(),
2242 ParticipantLocation::SharedProject {
2243 project_id: project_b_id
2244 }
2245 )]
2246 );
2247 assert!(b_notified.take());
2248 assert_eq!(
2249 participant_locations(&room_b, cx_b),
2250 vec![(
2251 "user_a".to_string(),
2252 ParticipantLocation::SharedProject {
2253 project_id: project_a_id
2254 }
2255 )]
2256 );
2257
2258 active_call_b
2259 .update(cx_b, |call, cx| call.set_location(None, cx))
2260 .await
2261 .unwrap();
2262 executor.run_until_parked();
2263 assert!(a_notified.take());
2264 assert_eq!(
2265 participant_locations(&room_a, cx_a),
2266 vec![("user_b".to_string(), ParticipantLocation::External)]
2267 );
2268 assert!(b_notified.take());
2269 assert_eq!(
2270 participant_locations(&room_b, cx_b),
2271 vec![(
2272 "user_a".to_string(),
2273 ParticipantLocation::SharedProject {
2274 project_id: project_a_id
2275 }
2276 )]
2277 );
2278
2279 fn participant_locations(
2280 room: &Entity<Room>,
2281 cx: &TestAppContext,
2282 ) -> Vec<(String, ParticipantLocation)> {
2283 room.read_with(cx, |room, _| {
2284 room.remote_participants()
2285 .values()
2286 .map(|participant| {
2287 (
2288 participant.user.github_login.to_string(),
2289 participant.location,
2290 )
2291 })
2292 .collect()
2293 })
2294 }
2295}
2296
2297#[gpui::test(iterations = 10)]
2298async fn test_propagate_saves_and_fs_changes(
2299 executor: BackgroundExecutor,
2300 cx_a: &mut TestAppContext,
2301 cx_b: &mut TestAppContext,
2302 cx_c: &mut TestAppContext,
2303) {
2304 let mut server = TestServer::start(executor.clone()).await;
2305 let client_a = server.create_client(cx_a, "user_a").await;
2306 let client_b = server.create_client(cx_b, "user_b").await;
2307 let client_c = server.create_client(cx_c, "user_c").await;
2308
2309 server
2310 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2311 .await;
2312 let active_call_a = cx_a.read(ActiveCall::global);
2313
2314 let rust = Arc::new(Language::new(
2315 LanguageConfig {
2316 name: "Rust".into(),
2317 matcher: LanguageMatcher {
2318 path_suffixes: vec!["rs".to_string()],
2319 ..Default::default()
2320 },
2321 ..Default::default()
2322 },
2323 Some(tree_sitter_rust::LANGUAGE.into()),
2324 ));
2325 let javascript = Arc::new(Language::new(
2326 LanguageConfig {
2327 name: "JavaScript".into(),
2328 matcher: LanguageMatcher {
2329 path_suffixes: vec!["js".to_string()],
2330 ..Default::default()
2331 },
2332 ..Default::default()
2333 },
2334 Some(tree_sitter_rust::LANGUAGE.into()),
2335 ));
2336 for client in [&client_a, &client_b, &client_c] {
2337 client.language_registry().add(rust.clone());
2338 client.language_registry().add(javascript.clone());
2339 }
2340
2341 client_a
2342 .fs()
2343 .insert_tree(
2344 path!("/a"),
2345 json!({
2346 "file1.rs": "",
2347 "file2": ""
2348 }),
2349 )
2350 .await;
2351 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2352
2353 let worktree_a = project_a.read_with(cx_a, |p, cx| p.worktrees(cx).next().unwrap());
2354 let project_id = active_call_a
2355 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2356 .await
2357 .unwrap();
2358
2359 // Join that worktree as clients B and C.
2360 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2361 let project_c = client_c.join_remote_project(project_id, cx_c).await;
2362
2363 let worktree_b = project_b.read_with(cx_b, |p, cx| p.worktrees(cx).next().unwrap());
2364
2365 let worktree_c = project_c.read_with(cx_c, |p, cx| p.worktrees(cx).next().unwrap());
2366
2367 // Open and edit a buffer as both guests B and C.
2368 let buffer_b = project_b
2369 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2370 .await
2371 .unwrap();
2372 let buffer_c = project_c
2373 .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2374 .await
2375 .unwrap();
2376
2377 buffer_b.read_with(cx_b, |buffer, _| {
2378 assert_eq!(buffer.language().unwrap().name(), "Rust".into());
2379 });
2380
2381 buffer_c.read_with(cx_c, |buffer, _| {
2382 assert_eq!(buffer.language().unwrap().name(), "Rust".into());
2383 });
2384 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], None, cx));
2385 buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], None, cx));
2386
2387 // Open and edit that buffer as the host.
2388 let buffer_a = project_a
2389 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2390 .await
2391 .unwrap();
2392
2393 executor.run_until_parked();
2394
2395 buffer_a.read_with(cx_a, |buf, _| assert_eq!(buf.text(), "i-am-c, i-am-b, "));
2396 buffer_a.update(cx_a, |buf, cx| {
2397 buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx)
2398 });
2399
2400 executor.run_until_parked();
2401
2402 buffer_a.read_with(cx_a, |buf, _| {
2403 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2404 });
2405
2406 buffer_b.read_with(cx_b, |buf, _| {
2407 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2408 });
2409
2410 buffer_c.read_with(cx_c, |buf, _| {
2411 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2412 });
2413
2414 // Edit the buffer as the host and concurrently save as guest B.
2415 let save_b = project_b.update(cx_b, |project, cx| {
2416 project.save_buffer(buffer_b.clone(), cx)
2417 });
2418 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
2419 save_b.await.unwrap();
2420 assert_eq!(
2421 client_a.fs().load("/a/file1.rs".as_ref()).await.unwrap(),
2422 "hi-a, i-am-c, i-am-b, i-am-a"
2423 );
2424
2425 executor.run_until_parked();
2426
2427 buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty()));
2428
2429 buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty()));
2430
2431 buffer_c.read_with(cx_c, |buf, _| assert!(!buf.is_dirty()));
2432
2433 // Make changes on host's file system, see those changes on guest worktrees.
2434 client_a
2435 .fs()
2436 .rename(
2437 path!("/a/file1.rs").as_ref(),
2438 path!("/a/file1.js").as_ref(),
2439 Default::default(),
2440 )
2441 .await
2442 .unwrap();
2443 client_a
2444 .fs()
2445 .rename(
2446 path!("/a/file2").as_ref(),
2447 path!("/a/file3").as_ref(),
2448 Default::default(),
2449 )
2450 .await
2451 .unwrap();
2452 client_a
2453 .fs()
2454 .insert_file(path!("/a/file4"), "4".into())
2455 .await;
2456 executor.run_until_parked();
2457
2458 worktree_a.read_with(cx_a, |tree, _| {
2459 assert_eq!(
2460 tree.paths()
2461 .map(|p| p.to_string_lossy())
2462 .collect::<Vec<_>>(),
2463 ["file1.js", "file3", "file4"]
2464 )
2465 });
2466
2467 worktree_b.read_with(cx_b, |tree, _| {
2468 assert_eq!(
2469 tree.paths()
2470 .map(|p| p.to_string_lossy())
2471 .collect::<Vec<_>>(),
2472 ["file1.js", "file3", "file4"]
2473 )
2474 });
2475
2476 worktree_c.read_with(cx_c, |tree, _| {
2477 assert_eq!(
2478 tree.paths()
2479 .map(|p| p.to_string_lossy())
2480 .collect::<Vec<_>>(),
2481 ["file1.js", "file3", "file4"]
2482 )
2483 });
2484
2485 // Ensure buffer files are updated as well.
2486
2487 buffer_a.read_with(cx_a, |buffer, _| {
2488 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2489 assert_eq!(buffer.language().unwrap().name(), "JavaScript".into());
2490 });
2491
2492 buffer_b.read_with(cx_b, |buffer, _| {
2493 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2494 assert_eq!(buffer.language().unwrap().name(), "JavaScript".into());
2495 });
2496
2497 buffer_c.read_with(cx_c, |buffer, _| {
2498 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2499 assert_eq!(buffer.language().unwrap().name(), "JavaScript".into());
2500 });
2501
2502 let new_buffer_a = project_a
2503 .update(cx_a, |p, cx| p.create_buffer(cx))
2504 .await
2505 .unwrap();
2506
2507 let new_buffer_id = new_buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id());
2508 let new_buffer_b = project_b
2509 .update(cx_b, |p, cx| p.open_buffer_by_id(new_buffer_id, cx))
2510 .await
2511 .unwrap();
2512
2513 new_buffer_b.read_with(cx_b, |buffer, _| {
2514 assert!(buffer.file().is_none());
2515 });
2516
2517 new_buffer_a.update(cx_a, |buffer, cx| {
2518 buffer.edit([(0..0, "ok")], None, cx);
2519 });
2520 project_a
2521 .update(cx_a, |project, cx| {
2522 let path = ProjectPath {
2523 path: Arc::from(Path::new("file3.rs")),
2524 worktree_id: worktree_a.read(cx).id(),
2525 };
2526
2527 project.save_buffer_as(new_buffer_a.clone(), path, cx)
2528 })
2529 .await
2530 .unwrap();
2531
2532 executor.run_until_parked();
2533
2534 new_buffer_b.read_with(cx_b, |buffer_b, _| {
2535 assert_eq!(
2536 buffer_b.file().unwrap().path().as_ref(),
2537 Path::new("file3.rs")
2538 );
2539
2540 new_buffer_a.read_with(cx_a, |buffer_a, _| {
2541 assert_eq!(buffer_b.saved_mtime(), buffer_a.saved_mtime());
2542 assert_eq!(buffer_b.saved_version(), buffer_a.saved_version());
2543 });
2544 });
2545}
2546
2547#[gpui::test(iterations = 10)]
2548async fn test_git_diff_base_change(
2549 executor: BackgroundExecutor,
2550 cx_a: &mut TestAppContext,
2551 cx_b: &mut TestAppContext,
2552) {
2553 let mut server = TestServer::start(executor.clone()).await;
2554 let client_a = server.create_client(cx_a, "user_a").await;
2555 let client_b = server.create_client(cx_b, "user_b").await;
2556 server
2557 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2558 .await;
2559 let active_call_a = cx_a.read(ActiveCall::global);
2560
2561 client_a
2562 .fs()
2563 .insert_tree(
2564 "/dir",
2565 json!({
2566 ".git": {},
2567 "sub": {
2568 ".git": {},
2569 "b.txt": "
2570 one
2571 two
2572 three
2573 ".unindent(),
2574 },
2575 "a.txt": "
2576 one
2577 two
2578 three
2579 ".unindent(),
2580 }),
2581 )
2582 .await;
2583
2584 let (project_local, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2585 let project_id = active_call_a
2586 .update(cx_a, |call, cx| {
2587 call.share_project(project_local.clone(), cx)
2588 })
2589 .await
2590 .unwrap();
2591
2592 let project_remote = client_b.join_remote_project(project_id, cx_b).await;
2593
2594 let staged_text = "
2595 one
2596 three
2597 "
2598 .unindent();
2599
2600 let committed_text = "
2601 one
2602 TWO
2603 three
2604 "
2605 .unindent();
2606
2607 let new_committed_text = "
2608 one
2609 TWO_HUNDRED
2610 three
2611 "
2612 .unindent();
2613
2614 let new_staged_text = "
2615 one
2616 two
2617 "
2618 .unindent();
2619
2620 client_a.fs().set_index_for_repo(
2621 Path::new("/dir/.git"),
2622 &[("a.txt".into(), staged_text.clone())],
2623 );
2624 client_a.fs().set_head_for_repo(
2625 Path::new("/dir/.git"),
2626 &[("a.txt".into(), committed_text.clone())],
2627 );
2628
2629 // Create the buffer
2630 let buffer_local_a = project_local
2631 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2632 .await
2633 .unwrap();
2634 let local_unstaged_diff_a = project_local
2635 .update(cx_a, |p, cx| {
2636 p.open_unstaged_diff(buffer_local_a.clone(), cx)
2637 })
2638 .await
2639 .unwrap();
2640
2641 // Wait for it to catch up to the new diff
2642 executor.run_until_parked();
2643 local_unstaged_diff_a.read_with(cx_a, |diff, cx| {
2644 let buffer = buffer_local_a.read(cx);
2645 assert_eq!(
2646 diff.base_text_string().as_deref(),
2647 Some(staged_text.as_str())
2648 );
2649 assert_hunks(
2650 diff.hunks_in_row_range(0..4, buffer, cx),
2651 buffer,
2652 &diff.base_text_string().unwrap(),
2653 &[(1..2, "", "two\n", DiffHunkStatus::added_none())],
2654 );
2655 });
2656
2657 // Create remote buffer
2658 let remote_buffer_a = project_remote
2659 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2660 .await
2661 .unwrap();
2662 let remote_unstaged_diff_a = project_remote
2663 .update(cx_b, |p, cx| {
2664 p.open_unstaged_diff(remote_buffer_a.clone(), cx)
2665 })
2666 .await
2667 .unwrap();
2668
2669 // Wait remote buffer to catch up to the new diff
2670 executor.run_until_parked();
2671 remote_unstaged_diff_a.read_with(cx_b, |diff, cx| {
2672 let buffer = remote_buffer_a.read(cx);
2673 assert_eq!(
2674 diff.base_text_string().as_deref(),
2675 Some(staged_text.as_str())
2676 );
2677 assert_hunks(
2678 diff.hunks_in_row_range(0..4, buffer, cx),
2679 buffer,
2680 &diff.base_text_string().unwrap(),
2681 &[(1..2, "", "two\n", DiffHunkStatus::added_none())],
2682 );
2683 });
2684
2685 // Open uncommitted changes on the guest, without opening them on the host first
2686 let remote_uncommitted_diff_a = project_remote
2687 .update(cx_b, |p, cx| {
2688 p.open_uncommitted_diff(remote_buffer_a.clone(), cx)
2689 })
2690 .await
2691 .unwrap();
2692 executor.run_until_parked();
2693 remote_uncommitted_diff_a.read_with(cx_b, |diff, cx| {
2694 let buffer = remote_buffer_a.read(cx);
2695 assert_eq!(
2696 diff.base_text_string().as_deref(),
2697 Some(committed_text.as_str())
2698 );
2699 assert_hunks(
2700 diff.hunks_in_row_range(0..4, buffer, cx),
2701 buffer,
2702 &diff.base_text_string().unwrap(),
2703 &[(
2704 1..2,
2705 "TWO\n",
2706 "two\n",
2707 DiffHunkStatus::modified(DiffHunkSecondaryStatus::HasSecondaryHunk),
2708 )],
2709 );
2710 });
2711
2712 // Update the index text of the open buffer
2713 client_a.fs().set_index_for_repo(
2714 Path::new("/dir/.git"),
2715 &[("a.txt".into(), new_staged_text.clone())],
2716 );
2717 client_a.fs().set_head_for_repo(
2718 Path::new("/dir/.git"),
2719 &[("a.txt".into(), new_committed_text.clone())],
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 );
3010 client_a.fs().set_index_for_repo(
3011 path!("/dir/.git").as_ref(),
3012 &[
3013 ("a.txt".into(), "".into()),
3014 ("b.txt".into(), "B".into()),
3015 ("c.txt".into(), "c".into()),
3016 ],
3017 );
3018 client_a.fs().set_unmerged_paths_for_repo(
3019 path!("/dir/.git").as_ref(),
3020 &[(
3021 "b.txt".into(),
3022 UnmergedStatus {
3023 first_head: UnmergedStatusCode::Updated,
3024 second_head: UnmergedStatusCode::Deleted,
3025 },
3026 )],
3027 );
3028
3029 const A_STATUS_START: FileStatus = FileStatus::Tracked(TrackedStatus {
3030 index_status: StatusCode::Added,
3031 worktree_status: StatusCode::Modified,
3032 });
3033 const B_STATUS_START: FileStatus = FileStatus::Unmerged(UnmergedStatus {
3034 first_head: UnmergedStatusCode::Updated,
3035 second_head: UnmergedStatusCode::Deleted,
3036 });
3037
3038 let (project_local, _worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
3039 let project_id = active_call_a
3040 .update(cx_a, |call, cx| {
3041 call.share_project(project_local.clone(), cx)
3042 })
3043 .await
3044 .unwrap();
3045
3046 let project_remote = client_b.join_remote_project(project_id, cx_b).await;
3047
3048 // Wait for it to catch up to the new status
3049 executor.run_until_parked();
3050
3051 #[track_caller]
3052 fn assert_status(
3053 file: impl AsRef<Path>,
3054 status: Option<FileStatus>,
3055 project: &Project,
3056 cx: &App,
3057 ) {
3058 let file = file.as_ref();
3059 let repos = project
3060 .repositories(cx)
3061 .values()
3062 .cloned()
3063 .collect::<Vec<_>>();
3064 assert_eq!(repos.len(), 1);
3065 let repo = repos.into_iter().next().unwrap();
3066 assert_eq!(
3067 repo.read(cx)
3068 .status_for_path(&file.into())
3069 .map(|entry| entry.status),
3070 status
3071 );
3072 }
3073
3074 project_local.read_with(cx_a, |project, cx| {
3075 assert_status("a.txt", Some(A_STATUS_START), project, cx);
3076 assert_status("b.txt", Some(B_STATUS_START), project, cx);
3077 assert_status("c.txt", None, project, cx);
3078 });
3079
3080 project_remote.read_with(cx_b, |project, cx| {
3081 assert_status("a.txt", Some(A_STATUS_START), project, cx);
3082 assert_status("b.txt", Some(B_STATUS_START), project, cx);
3083 assert_status("c.txt", None, project, cx);
3084 });
3085
3086 const A_STATUS_END: FileStatus = FileStatus::Tracked(TrackedStatus {
3087 index_status: StatusCode::Added,
3088 worktree_status: StatusCode::Unmodified,
3089 });
3090 const B_STATUS_END: FileStatus = FileStatus::Tracked(TrackedStatus {
3091 index_status: StatusCode::Deleted,
3092 worktree_status: StatusCode::Added,
3093 });
3094 const C_STATUS_END: FileStatus = FileStatus::Tracked(TrackedStatus {
3095 index_status: StatusCode::Unmodified,
3096 worktree_status: StatusCode::Modified,
3097 });
3098
3099 // Delete b.txt from the index, mark conflict as resolved,
3100 // and modify c.txt in the working copy.
3101 client_a.fs().set_index_for_repo(
3102 path!("/dir/.git").as_ref(),
3103 &[("a.txt".into(), "a".into()), ("c.txt".into(), "c".into())],
3104 );
3105 client_a
3106 .fs()
3107 .set_unmerged_paths_for_repo(path!("/dir/.git").as_ref(), &[]);
3108 client_a
3109 .fs()
3110 .atomic_write(path!("/dir/c.txt").into(), "CC".into())
3111 .await
3112 .unwrap();
3113
3114 // Wait for buffer_local_a to receive it
3115 executor.run_until_parked();
3116
3117 // Smoke test status reading
3118 project_local.read_with(cx_a, |project, cx| {
3119 assert_status("a.txt", Some(A_STATUS_END), project, cx);
3120 assert_status("b.txt", Some(B_STATUS_END), project, cx);
3121 assert_status("c.txt", Some(C_STATUS_END), project, cx);
3122 });
3123
3124 project_remote.read_with(cx_b, |project, cx| {
3125 assert_status("a.txt", Some(A_STATUS_END), project, cx);
3126 assert_status("b.txt", Some(B_STATUS_END), project, cx);
3127 assert_status("c.txt", Some(C_STATUS_END), project, cx);
3128 });
3129
3130 // And synchronization while joining
3131 let project_remote_c = client_c.join_remote_project(project_id, cx_c).await;
3132 executor.run_until_parked();
3133
3134 project_remote_c.read_with(cx_c, |project, cx| {
3135 assert_status("a.txt", Some(A_STATUS_END), project, cx);
3136 assert_status("b.txt", Some(B_STATUS_END), project, cx);
3137 assert_status("c.txt", Some(C_STATUS_END), project, cx);
3138 });
3139
3140 // Now remove the original git repository and check that collaborators are notified.
3141 client_a
3142 .fs()
3143 .remove_dir(path!("/dir/.git").as_ref(), RemoveOptions::default())
3144 .await
3145 .unwrap();
3146
3147 executor.run_until_parked();
3148 project_remote.update(cx_b, |project, cx| {
3149 pretty_assertions::assert_eq!(
3150 project.git_store().read(cx).repo_snapshots(cx),
3151 HashMap::default()
3152 );
3153 });
3154 project_remote_c.update(cx_c, |project, cx| {
3155 pretty_assertions::assert_eq!(
3156 project.git_store().read(cx).repo_snapshots(cx),
3157 HashMap::default()
3158 );
3159 });
3160}
3161
3162#[gpui::test(iterations = 10)]
3163async fn test_fs_operations(
3164 executor: BackgroundExecutor,
3165 cx_a: &mut TestAppContext,
3166 cx_b: &mut TestAppContext,
3167) {
3168 let mut server = TestServer::start(executor.clone()).await;
3169 let client_a = server.create_client(cx_a, "user_a").await;
3170 let client_b = server.create_client(cx_b, "user_b").await;
3171 server
3172 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3173 .await;
3174 let active_call_a = cx_a.read(ActiveCall::global);
3175
3176 client_a
3177 .fs()
3178 .insert_tree(
3179 path!("/dir"),
3180 json!({
3181 "a.txt": "a-contents",
3182 "b.txt": "b-contents",
3183 }),
3184 )
3185 .await;
3186 let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
3187 let project_id = active_call_a
3188 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3189 .await
3190 .unwrap();
3191 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3192
3193 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
3194 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
3195
3196 let entry = project_b
3197 .update(cx_b, |project, cx| {
3198 project.create_entry((worktree_id, "c.txt"), false, cx)
3199 })
3200 .await
3201 .unwrap()
3202 .to_included()
3203 .unwrap();
3204
3205 worktree_a.read_with(cx_a, |worktree, _| {
3206 assert_eq!(
3207 worktree
3208 .paths()
3209 .map(|p| p.to_string_lossy())
3210 .collect::<Vec<_>>(),
3211 ["a.txt", "b.txt", "c.txt"]
3212 );
3213 });
3214
3215 worktree_b.read_with(cx_b, |worktree, _| {
3216 assert_eq!(
3217 worktree
3218 .paths()
3219 .map(|p| p.to_string_lossy())
3220 .collect::<Vec<_>>(),
3221 ["a.txt", "b.txt", "c.txt"]
3222 );
3223 });
3224
3225 project_b
3226 .update(cx_b, |project, cx| {
3227 project.rename_entry(entry.id, Path::new("d.txt"), cx)
3228 })
3229 .await
3230 .unwrap()
3231 .to_included()
3232 .unwrap();
3233
3234 worktree_a.read_with(cx_a, |worktree, _| {
3235 assert_eq!(
3236 worktree
3237 .paths()
3238 .map(|p| p.to_string_lossy())
3239 .collect::<Vec<_>>(),
3240 ["a.txt", "b.txt", "d.txt"]
3241 );
3242 });
3243
3244 worktree_b.read_with(cx_b, |worktree, _| {
3245 assert_eq!(
3246 worktree
3247 .paths()
3248 .map(|p| p.to_string_lossy())
3249 .collect::<Vec<_>>(),
3250 ["a.txt", "b.txt", "d.txt"]
3251 );
3252 });
3253
3254 let dir_entry = project_b
3255 .update(cx_b, |project, cx| {
3256 project.create_entry((worktree_id, "DIR"), true, cx)
3257 })
3258 .await
3259 .unwrap()
3260 .to_included()
3261 .unwrap();
3262
3263 worktree_a.read_with(cx_a, |worktree, _| {
3264 assert_eq!(
3265 worktree
3266 .paths()
3267 .map(|p| p.to_string_lossy())
3268 .collect::<Vec<_>>(),
3269 ["DIR", "a.txt", "b.txt", "d.txt"]
3270 );
3271 });
3272
3273 worktree_b.read_with(cx_b, |worktree, _| {
3274 assert_eq!(
3275 worktree
3276 .paths()
3277 .map(|p| p.to_string_lossy())
3278 .collect::<Vec<_>>(),
3279 ["DIR", "a.txt", "b.txt", "d.txt"]
3280 );
3281 });
3282
3283 project_b
3284 .update(cx_b, |project, cx| {
3285 project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
3286 })
3287 .await
3288 .unwrap()
3289 .to_included()
3290 .unwrap();
3291
3292 project_b
3293 .update(cx_b, |project, cx| {
3294 project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
3295 })
3296 .await
3297 .unwrap()
3298 .to_included()
3299 .unwrap();
3300
3301 project_b
3302 .update(cx_b, |project, cx| {
3303 project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
3304 })
3305 .await
3306 .unwrap()
3307 .to_included()
3308 .unwrap();
3309
3310 worktree_a.read_with(cx_a, |worktree, _| {
3311 assert_eq!(
3312 worktree
3313 .paths()
3314 .map(|p| p.to_string_lossy())
3315 .collect::<Vec<_>>(),
3316 [
3317 separator!("DIR"),
3318 separator!("DIR/SUBDIR"),
3319 separator!("DIR/SUBDIR/f.txt"),
3320 separator!("DIR/e.txt"),
3321 separator!("a.txt"),
3322 separator!("b.txt"),
3323 separator!("d.txt")
3324 ]
3325 );
3326 });
3327
3328 worktree_b.read_with(cx_b, |worktree, _| {
3329 assert_eq!(
3330 worktree
3331 .paths()
3332 .map(|p| p.to_string_lossy())
3333 .collect::<Vec<_>>(),
3334 [
3335 separator!("DIR"),
3336 separator!("DIR/SUBDIR"),
3337 separator!("DIR/SUBDIR/f.txt"),
3338 separator!("DIR/e.txt"),
3339 separator!("a.txt"),
3340 separator!("b.txt"),
3341 separator!("d.txt")
3342 ]
3343 );
3344 });
3345
3346 project_b
3347 .update(cx_b, |project, cx| {
3348 project.copy_entry(entry.id, None, Path::new("f.txt"), cx)
3349 })
3350 .await
3351 .unwrap()
3352 .unwrap();
3353
3354 worktree_a.read_with(cx_a, |worktree, _| {
3355 assert_eq!(
3356 worktree
3357 .paths()
3358 .map(|p| p.to_string_lossy())
3359 .collect::<Vec<_>>(),
3360 [
3361 separator!("DIR"),
3362 separator!("DIR/SUBDIR"),
3363 separator!("DIR/SUBDIR/f.txt"),
3364 separator!("DIR/e.txt"),
3365 separator!("a.txt"),
3366 separator!("b.txt"),
3367 separator!("d.txt"),
3368 separator!("f.txt")
3369 ]
3370 );
3371 });
3372
3373 worktree_b.read_with(cx_b, |worktree, _| {
3374 assert_eq!(
3375 worktree
3376 .paths()
3377 .map(|p| p.to_string_lossy())
3378 .collect::<Vec<_>>(),
3379 [
3380 separator!("DIR"),
3381 separator!("DIR/SUBDIR"),
3382 separator!("DIR/SUBDIR/f.txt"),
3383 separator!("DIR/e.txt"),
3384 separator!("a.txt"),
3385 separator!("b.txt"),
3386 separator!("d.txt"),
3387 separator!("f.txt")
3388 ]
3389 );
3390 });
3391
3392 project_b
3393 .update(cx_b, |project, cx| {
3394 project.delete_entry(dir_entry.id, false, cx).unwrap()
3395 })
3396 .await
3397 .unwrap();
3398 executor.run_until_parked();
3399
3400 worktree_a.read_with(cx_a, |worktree, _| {
3401 assert_eq!(
3402 worktree
3403 .paths()
3404 .map(|p| p.to_string_lossy())
3405 .collect::<Vec<_>>(),
3406 ["a.txt", "b.txt", "d.txt", "f.txt"]
3407 );
3408 });
3409
3410 worktree_b.read_with(cx_b, |worktree, _| {
3411 assert_eq!(
3412 worktree
3413 .paths()
3414 .map(|p| p.to_string_lossy())
3415 .collect::<Vec<_>>(),
3416 ["a.txt", "b.txt", "d.txt", "f.txt"]
3417 );
3418 });
3419
3420 project_b
3421 .update(cx_b, |project, cx| {
3422 project.delete_entry(entry.id, false, cx).unwrap()
3423 })
3424 .await
3425 .unwrap();
3426
3427 worktree_a.read_with(cx_a, |worktree, _| {
3428 assert_eq!(
3429 worktree
3430 .paths()
3431 .map(|p| p.to_string_lossy())
3432 .collect::<Vec<_>>(),
3433 ["a.txt", "b.txt", "f.txt"]
3434 );
3435 });
3436
3437 worktree_b.read_with(cx_b, |worktree, _| {
3438 assert_eq!(
3439 worktree
3440 .paths()
3441 .map(|p| p.to_string_lossy())
3442 .collect::<Vec<_>>(),
3443 ["a.txt", "b.txt", "f.txt"]
3444 );
3445 });
3446}
3447
3448#[gpui::test(iterations = 10)]
3449async fn test_local_settings(
3450 executor: BackgroundExecutor,
3451 cx_a: &mut TestAppContext,
3452 cx_b: &mut TestAppContext,
3453) {
3454 let mut server = TestServer::start(executor.clone()).await;
3455 let client_a = server.create_client(cx_a, "user_a").await;
3456 let client_b = server.create_client(cx_b, "user_b").await;
3457 server
3458 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3459 .await;
3460 let active_call_a = cx_a.read(ActiveCall::global);
3461
3462 // As client A, open a project that contains some local settings files
3463 client_a
3464 .fs()
3465 .insert_tree(
3466 "/dir",
3467 json!({
3468 ".zed": {
3469 "settings.json": r#"{ "tab_size": 2 }"#
3470 },
3471 "a": {
3472 ".zed": {
3473 "settings.json": r#"{ "tab_size": 8 }"#
3474 },
3475 "a.txt": "a-contents",
3476 },
3477 "b": {
3478 "b.txt": "b-contents",
3479 }
3480 }),
3481 )
3482 .await;
3483 let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
3484 executor.run_until_parked();
3485 let project_id = active_call_a
3486 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3487 .await
3488 .unwrap();
3489 executor.run_until_parked();
3490
3491 // As client B, join that project and observe the local settings.
3492 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3493
3494 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
3495 executor.run_until_parked();
3496 cx_b.read(|cx| {
3497 let store = cx.global::<SettingsStore>();
3498 assert_eq!(
3499 store
3500 .local_settings(worktree_b.read(cx).id())
3501 .collect::<Vec<_>>(),
3502 &[
3503 (Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
3504 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3505 ]
3506 )
3507 });
3508
3509 // As client A, update a settings file. As Client B, see the changed settings.
3510 client_a
3511 .fs()
3512 .insert_file("/dir/.zed/settings.json", r#"{}"#.into())
3513 .await;
3514 executor.run_until_parked();
3515 cx_b.read(|cx| {
3516 let store = cx.global::<SettingsStore>();
3517 assert_eq!(
3518 store
3519 .local_settings(worktree_b.read(cx).id())
3520 .collect::<Vec<_>>(),
3521 &[
3522 (Path::new("").into(), r#"{}"#.to_string()),
3523 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3524 ]
3525 )
3526 });
3527
3528 // As client A, create and remove some settings files. As client B, see the changed settings.
3529 client_a
3530 .fs()
3531 .remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
3532 .await
3533 .unwrap();
3534 client_a
3535 .fs()
3536 .create_dir("/dir/b/.zed".as_ref())
3537 .await
3538 .unwrap();
3539 client_a
3540 .fs()
3541 .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
3542 .await;
3543 executor.run_until_parked();
3544 cx_b.read(|cx| {
3545 let store = cx.global::<SettingsStore>();
3546 assert_eq!(
3547 store
3548 .local_settings(worktree_b.read(cx).id())
3549 .collect::<Vec<_>>(),
3550 &[
3551 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3552 (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
3553 ]
3554 )
3555 });
3556
3557 // As client B, disconnect.
3558 server.forbid_connections();
3559 server.disconnect_client(client_b.peer_id().unwrap());
3560
3561 // As client A, change and remove settings files while client B is disconnected.
3562 client_a
3563 .fs()
3564 .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
3565 .await;
3566 client_a
3567 .fs()
3568 .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
3569 .await
3570 .unwrap();
3571 executor.run_until_parked();
3572
3573 // As client B, reconnect and see the changed settings.
3574 server.allow_connections();
3575 executor.advance_clock(RECEIVE_TIMEOUT);
3576 cx_b.read(|cx| {
3577 let store = cx.global::<SettingsStore>();
3578 assert_eq!(
3579 store
3580 .local_settings(worktree_b.read(cx).id())
3581 .collect::<Vec<_>>(),
3582 &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
3583 )
3584 });
3585}
3586
3587#[gpui::test(iterations = 10)]
3588async fn test_buffer_conflict_after_save(
3589 executor: BackgroundExecutor,
3590 cx_a: &mut TestAppContext,
3591 cx_b: &mut TestAppContext,
3592) {
3593 let mut server = TestServer::start(executor.clone()).await;
3594 let client_a = server.create_client(cx_a, "user_a").await;
3595 let client_b = server.create_client(cx_b, "user_b").await;
3596 server
3597 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3598 .await;
3599 let active_call_a = cx_a.read(ActiveCall::global);
3600
3601 client_a
3602 .fs()
3603 .insert_tree(
3604 path!("/dir"),
3605 json!({
3606 "a.txt": "a-contents",
3607 }),
3608 )
3609 .await;
3610 let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
3611 let project_id = active_call_a
3612 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3613 .await
3614 .unwrap();
3615 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3616
3617 // Open a buffer as client B
3618 let buffer_b = project_b
3619 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3620 .await
3621 .unwrap();
3622
3623 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
3624
3625 buffer_b.read_with(cx_b, |buf, _| {
3626 assert!(buf.is_dirty());
3627 assert!(!buf.has_conflict());
3628 });
3629
3630 project_b
3631 .update(cx_b, |project, cx| {
3632 project.save_buffer(buffer_b.clone(), cx)
3633 })
3634 .await
3635 .unwrap();
3636
3637 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
3638
3639 buffer_b.read_with(cx_b, |buf, _| {
3640 assert!(!buf.has_conflict());
3641 });
3642
3643 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
3644
3645 buffer_b.read_with(cx_b, |buf, _| {
3646 assert!(buf.is_dirty());
3647 assert!(!buf.has_conflict());
3648 });
3649}
3650
3651#[gpui::test(iterations = 10)]
3652async fn test_buffer_reloading(
3653 executor: BackgroundExecutor,
3654 cx_a: &mut TestAppContext,
3655 cx_b: &mut TestAppContext,
3656) {
3657 let mut server = TestServer::start(executor.clone()).await;
3658 let client_a = server.create_client(cx_a, "user_a").await;
3659 let client_b = server.create_client(cx_b, "user_b").await;
3660 server
3661 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3662 .await;
3663 let active_call_a = cx_a.read(ActiveCall::global);
3664
3665 client_a
3666 .fs()
3667 .insert_tree(
3668 path!("/dir"),
3669 json!({
3670 "a.txt": "a\nb\nc",
3671 }),
3672 )
3673 .await;
3674 let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
3675 let project_id = active_call_a
3676 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3677 .await
3678 .unwrap();
3679 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3680
3681 // Open a buffer as client B
3682 let buffer_b = project_b
3683 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3684 .await
3685 .unwrap();
3686
3687 buffer_b.read_with(cx_b, |buf, _| {
3688 assert!(!buf.is_dirty());
3689 assert!(!buf.has_conflict());
3690 assert_eq!(buf.line_ending(), LineEnding::Unix);
3691 });
3692
3693 let new_contents = Rope::from("d\ne\nf");
3694 client_a
3695 .fs()
3696 .save(
3697 path!("/dir/a.txt").as_ref(),
3698 &new_contents,
3699 LineEnding::Windows,
3700 )
3701 .await
3702 .unwrap();
3703
3704 executor.run_until_parked();
3705
3706 buffer_b.read_with(cx_b, |buf, _| {
3707 assert_eq!(buf.text(), new_contents.to_string());
3708 assert!(!buf.is_dirty());
3709 assert!(!buf.has_conflict());
3710 assert_eq!(buf.line_ending(), LineEnding::Windows);
3711 });
3712}
3713
3714#[gpui::test(iterations = 10)]
3715async fn test_editing_while_guest_opens_buffer(
3716 executor: BackgroundExecutor,
3717 cx_a: &mut TestAppContext,
3718 cx_b: &mut TestAppContext,
3719) {
3720 let mut server = TestServer::start(executor.clone()).await;
3721 let client_a = server.create_client(cx_a, "user_a").await;
3722 let client_b = server.create_client(cx_b, "user_b").await;
3723 server
3724 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3725 .await;
3726 let active_call_a = cx_a.read(ActiveCall::global);
3727
3728 client_a
3729 .fs()
3730 .insert_tree(path!("/dir"), json!({ "a.txt": "a-contents" }))
3731 .await;
3732 let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
3733 let project_id = active_call_a
3734 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3735 .await
3736 .unwrap();
3737 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3738
3739 // Open a buffer as client A
3740 let buffer_a = project_a
3741 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3742 .await
3743 .unwrap();
3744
3745 // Start opening the same buffer as client B
3746 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3747 let buffer_b = cx_b.executor().spawn(open_buffer);
3748
3749 // Edit the buffer as client A while client B is still opening it.
3750 cx_b.executor().simulate_random_delay().await;
3751 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
3752 cx_b.executor().simulate_random_delay().await;
3753 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
3754
3755 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3756 let buffer_b = buffer_b.await.unwrap();
3757 executor.run_until_parked();
3758
3759 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3760}
3761
3762#[gpui::test(iterations = 10)]
3763async fn test_leaving_worktree_while_opening_buffer(
3764 executor: BackgroundExecutor,
3765 cx_a: &mut TestAppContext,
3766 cx_b: &mut TestAppContext,
3767) {
3768 let mut server = TestServer::start(executor.clone()).await;
3769 let client_a = server.create_client(cx_a, "user_a").await;
3770 let client_b = server.create_client(cx_b, "user_b").await;
3771 server
3772 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3773 .await;
3774 let active_call_a = cx_a.read(ActiveCall::global);
3775
3776 client_a
3777 .fs()
3778 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3779 .await;
3780 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3781 let project_id = active_call_a
3782 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3783 .await
3784 .unwrap();
3785 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3786
3787 // See that a guest has joined as client A.
3788 executor.run_until_parked();
3789
3790 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3791
3792 // Begin opening a buffer as client B, but leave the project before the open completes.
3793 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3794 let buffer_b = cx_b.executor().spawn(open_buffer);
3795 cx_b.update(|_| drop(project_b));
3796 drop(buffer_b);
3797
3798 // See that the guest has left.
3799 executor.run_until_parked();
3800
3801 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3802}
3803
3804#[gpui::test(iterations = 10)]
3805async fn test_canceling_buffer_opening(
3806 executor: BackgroundExecutor,
3807 cx_a: &mut TestAppContext,
3808 cx_b: &mut TestAppContext,
3809) {
3810 let mut server = TestServer::start(executor.clone()).await;
3811 let client_a = server.create_client(cx_a, "user_a").await;
3812 let client_b = server.create_client(cx_b, "user_b").await;
3813 server
3814 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3815 .await;
3816 let active_call_a = cx_a.read(ActiveCall::global);
3817
3818 client_a
3819 .fs()
3820 .insert_tree(
3821 "/dir",
3822 json!({
3823 "a.txt": "abc",
3824 }),
3825 )
3826 .await;
3827 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3828 let project_id = active_call_a
3829 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3830 .await
3831 .unwrap();
3832 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3833
3834 let buffer_a = project_a
3835 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3836 .await
3837 .unwrap();
3838
3839 // Open a buffer as client B but cancel after a random amount of time.
3840 let buffer_b = project_b.update(cx_b, |p, cx| {
3841 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3842 });
3843 executor.simulate_random_delay().await;
3844 drop(buffer_b);
3845
3846 // Try opening the same buffer again as client B, and ensure we can
3847 // still do it despite the cancellation above.
3848 let buffer_b = project_b
3849 .update(cx_b, |p, cx| {
3850 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3851 })
3852 .await
3853 .unwrap();
3854
3855 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3856}
3857
3858#[gpui::test(iterations = 10)]
3859async fn test_leaving_project(
3860 executor: BackgroundExecutor,
3861 cx_a: &mut TestAppContext,
3862 cx_b: &mut TestAppContext,
3863 cx_c: &mut TestAppContext,
3864) {
3865 let mut server = TestServer::start(executor.clone()).await;
3866 let client_a = server.create_client(cx_a, "user_a").await;
3867 let client_b = server.create_client(cx_b, "user_b").await;
3868 let client_c = server.create_client(cx_c, "user_c").await;
3869 server
3870 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3871 .await;
3872 let active_call_a = cx_a.read(ActiveCall::global);
3873
3874 client_a
3875 .fs()
3876 .insert_tree(
3877 "/a",
3878 json!({
3879 "a.txt": "a-contents",
3880 "b.txt": "b-contents",
3881 }),
3882 )
3883 .await;
3884 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3885 let project_id = active_call_a
3886 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3887 .await
3888 .unwrap();
3889 let project_b1 = client_b.join_remote_project(project_id, cx_b).await;
3890 let project_c = client_c.join_remote_project(project_id, cx_c).await;
3891
3892 // Client A sees that a guest has joined.
3893 executor.run_until_parked();
3894
3895 project_a.read_with(cx_a, |project, _| {
3896 assert_eq!(project.collaborators().len(), 2);
3897 });
3898
3899 project_b1.read_with(cx_b, |project, _| {
3900 assert_eq!(project.collaborators().len(), 2);
3901 });
3902
3903 project_c.read_with(cx_c, |project, _| {
3904 assert_eq!(project.collaborators().len(), 2);
3905 });
3906
3907 // Client B opens a buffer.
3908 let buffer_b1 = project_b1
3909 .update(cx_b, |project, cx| {
3910 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3911 project.open_buffer((worktree_id, "a.txt"), cx)
3912 })
3913 .await
3914 .unwrap();
3915
3916 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3917
3918 // Drop client B's project and ensure client A and client C observe client B leaving.
3919 cx_b.update(|_| drop(project_b1));
3920 executor.run_until_parked();
3921
3922 project_a.read_with(cx_a, |project, _| {
3923 assert_eq!(project.collaborators().len(), 1);
3924 });
3925
3926 project_c.read_with(cx_c, |project, _| {
3927 assert_eq!(project.collaborators().len(), 1);
3928 });
3929
3930 // Client B re-joins the project and can open buffers as before.
3931 let project_b2 = client_b.join_remote_project(project_id, cx_b).await;
3932 executor.run_until_parked();
3933
3934 project_a.read_with(cx_a, |project, _| {
3935 assert_eq!(project.collaborators().len(), 2);
3936 });
3937
3938 project_b2.read_with(cx_b, |project, _| {
3939 assert_eq!(project.collaborators().len(), 2);
3940 });
3941
3942 project_c.read_with(cx_c, |project, _| {
3943 assert_eq!(project.collaborators().len(), 2);
3944 });
3945
3946 let buffer_b2 = project_b2
3947 .update(cx_b, |project, cx| {
3948 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3949 project.open_buffer((worktree_id, "a.txt"), cx)
3950 })
3951 .await
3952 .unwrap();
3953
3954 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3955
3956 project_a.read_with(cx_a, |project, _| {
3957 assert_eq!(project.collaborators().len(), 2);
3958 });
3959
3960 // Drop client B's connection and ensure client A and client C observe client B leaving.
3961 client_b.disconnect(&cx_b.to_async());
3962 executor.advance_clock(RECONNECT_TIMEOUT);
3963
3964 project_a.read_with(cx_a, |project, _| {
3965 assert_eq!(project.collaborators().len(), 1);
3966 });
3967
3968 project_b2.read_with(cx_b, |project, cx| {
3969 assert!(project.is_disconnected(cx));
3970 });
3971
3972 project_c.read_with(cx_c, |project, _| {
3973 assert_eq!(project.collaborators().len(), 1);
3974 });
3975
3976 // Client B can't join the project, unless they re-join the room.
3977 cx_b.spawn(|cx| {
3978 Project::in_room(
3979 project_id,
3980 client_b.app_state.client.clone(),
3981 client_b.user_store().clone(),
3982 client_b.language_registry().clone(),
3983 FakeFs::new(cx.background_executor().clone()),
3984 cx,
3985 )
3986 })
3987 .await
3988 .unwrap_err();
3989
3990 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3991 client_c.wait_for_current_user(cx_c).await;
3992 server.forbid_connections();
3993 server.disconnect_client(client_c.peer_id().unwrap());
3994 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
3995 executor.run_until_parked();
3996
3997 project_a.read_with(cx_a, |project, _| {
3998 assert_eq!(project.collaborators().len(), 0);
3999 });
4000
4001 project_b2.read_with(cx_b, |project, cx| {
4002 assert!(project.is_disconnected(cx));
4003 });
4004
4005 project_c.read_with(cx_c, |project, cx| {
4006 assert!(project.is_disconnected(cx));
4007 });
4008}
4009
4010#[gpui::test(iterations = 10)]
4011async fn test_collaborating_with_diagnostics(
4012 executor: BackgroundExecutor,
4013 cx_a: &mut TestAppContext,
4014 cx_b: &mut TestAppContext,
4015 cx_c: &mut TestAppContext,
4016) {
4017 let mut server = TestServer::start(executor.clone()).await;
4018 let client_a = server.create_client(cx_a, "user_a").await;
4019 let client_b = server.create_client(cx_b, "user_b").await;
4020 let client_c = server.create_client(cx_c, "user_c").await;
4021 server
4022 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
4023 .await;
4024 let active_call_a = cx_a.read(ActiveCall::global);
4025
4026 client_a.language_registry().add(Arc::new(Language::new(
4027 LanguageConfig {
4028 name: "Rust".into(),
4029 matcher: LanguageMatcher {
4030 path_suffixes: vec!["rs".to_string()],
4031 ..Default::default()
4032 },
4033 ..Default::default()
4034 },
4035 Some(tree_sitter_rust::LANGUAGE.into()),
4036 )));
4037 let mut fake_language_servers = client_a
4038 .language_registry()
4039 .register_fake_lsp("Rust", Default::default());
4040
4041 // Share a project as client A
4042 client_a
4043 .fs()
4044 .insert_tree(
4045 path!("/a"),
4046 json!({
4047 "a.rs": "let one = two",
4048 "other.rs": "",
4049 }),
4050 )
4051 .await;
4052 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
4053
4054 // Cause the language server to start.
4055 let _buffer = project_a
4056 .update(cx_a, |project, cx| {
4057 project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
4058 })
4059 .await
4060 .unwrap();
4061
4062 // Simulate a language server reporting errors for a file.
4063 let mut fake_language_server = fake_language_servers.next().await.unwrap();
4064 fake_language_server
4065 .receive_notification::<lsp::notification::DidOpenTextDocument>()
4066 .await;
4067 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4068 &lsp::PublishDiagnosticsParams {
4069 uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
4070 version: None,
4071 diagnostics: vec![lsp::Diagnostic {
4072 severity: Some(lsp::DiagnosticSeverity::WARNING),
4073 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
4074 message: "message 0".to_string(),
4075 ..Default::default()
4076 }],
4077 },
4078 );
4079
4080 // Client A shares the project and, simultaneously, the language server
4081 // publishes a diagnostic. This is done to ensure that the server always
4082 // observes the latest diagnostics for a worktree.
4083 let project_id = active_call_a
4084 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4085 .await
4086 .unwrap();
4087 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4088 &lsp::PublishDiagnosticsParams {
4089 uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
4090 version: None,
4091 diagnostics: vec![lsp::Diagnostic {
4092 severity: Some(lsp::DiagnosticSeverity::ERROR),
4093 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
4094 message: "message 1".to_string(),
4095 ..Default::default()
4096 }],
4097 },
4098 );
4099
4100 // Join the worktree as client B.
4101 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4102
4103 // Wait for server to see the diagnostics update.
4104 executor.run_until_parked();
4105
4106 // Ensure client B observes the new diagnostics.
4107
4108 project_b.read_with(cx_b, |project, cx| {
4109 assert_eq!(
4110 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4111 &[(
4112 ProjectPath {
4113 worktree_id,
4114 path: Arc::from(Path::new("a.rs")),
4115 },
4116 LanguageServerId(0),
4117 DiagnosticSummary {
4118 error_count: 1,
4119 warning_count: 0,
4120 },
4121 )]
4122 )
4123 });
4124
4125 // Join project as client C and observe the diagnostics.
4126 let project_c = client_c.join_remote_project(project_id, cx_c).await;
4127 executor.run_until_parked();
4128 let project_c_diagnostic_summaries =
4129 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
4130 project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
4131 })));
4132 project_c.update(cx_c, |_, cx| {
4133 let summaries = project_c_diagnostic_summaries.clone();
4134 cx.subscribe(&project_c, {
4135 move |p, _, event, cx| {
4136 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
4137 *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
4138 }
4139 }
4140 })
4141 .detach();
4142 });
4143
4144 executor.run_until_parked();
4145 assert_eq!(
4146 project_c_diagnostic_summaries.borrow().as_slice(),
4147 &[(
4148 ProjectPath {
4149 worktree_id,
4150 path: Arc::from(Path::new("a.rs")),
4151 },
4152 LanguageServerId(0),
4153 DiagnosticSummary {
4154 error_count: 1,
4155 warning_count: 0,
4156 },
4157 )]
4158 );
4159
4160 // Simulate a language server reporting more errors for a file.
4161 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4162 &lsp::PublishDiagnosticsParams {
4163 uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
4164 version: None,
4165 diagnostics: vec![
4166 lsp::Diagnostic {
4167 severity: Some(lsp::DiagnosticSeverity::ERROR),
4168 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
4169 message: "message 1".to_string(),
4170 ..Default::default()
4171 },
4172 lsp::Diagnostic {
4173 severity: Some(lsp::DiagnosticSeverity::WARNING),
4174 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
4175 message: "message 2".to_string(),
4176 ..Default::default()
4177 },
4178 ],
4179 },
4180 );
4181
4182 // Clients B and C get the updated summaries
4183 executor.run_until_parked();
4184
4185 project_b.read_with(cx_b, |project, cx| {
4186 assert_eq!(
4187 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4188 [(
4189 ProjectPath {
4190 worktree_id,
4191 path: Arc::from(Path::new("a.rs")),
4192 },
4193 LanguageServerId(0),
4194 DiagnosticSummary {
4195 error_count: 1,
4196 warning_count: 1,
4197 },
4198 )]
4199 );
4200 });
4201
4202 project_c.read_with(cx_c, |project, cx| {
4203 assert_eq!(
4204 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4205 [(
4206 ProjectPath {
4207 worktree_id,
4208 path: Arc::from(Path::new("a.rs")),
4209 },
4210 LanguageServerId(0),
4211 DiagnosticSummary {
4212 error_count: 1,
4213 warning_count: 1,
4214 },
4215 )]
4216 );
4217 });
4218
4219 // Open the file with the errors on client B. They should be present.
4220 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4221 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4222
4223 buffer_b.read_with(cx_b, |buffer, _| {
4224 assert_eq!(
4225 buffer
4226 .snapshot()
4227 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
4228 .collect::<Vec<_>>(),
4229 &[
4230 DiagnosticEntry {
4231 range: Point::new(0, 4)..Point::new(0, 7),
4232 diagnostic: Diagnostic {
4233 group_id: 2,
4234 message: "message 1".to_string(),
4235 severity: lsp::DiagnosticSeverity::ERROR,
4236 is_primary: true,
4237 ..Default::default()
4238 }
4239 },
4240 DiagnosticEntry {
4241 range: Point::new(0, 10)..Point::new(0, 13),
4242 diagnostic: Diagnostic {
4243 group_id: 3,
4244 severity: lsp::DiagnosticSeverity::WARNING,
4245 message: "message 2".to_string(),
4246 is_primary: true,
4247 ..Default::default()
4248 }
4249 }
4250 ]
4251 );
4252 });
4253
4254 // Simulate a language server reporting no errors for a file.
4255 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4256 &lsp::PublishDiagnosticsParams {
4257 uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
4258 version: None,
4259 diagnostics: vec![],
4260 },
4261 );
4262 executor.run_until_parked();
4263
4264 project_a.read_with(cx_a, |project, cx| {
4265 assert_eq!(
4266 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4267 []
4268 )
4269 });
4270
4271 project_b.read_with(cx_b, |project, cx| {
4272 assert_eq!(
4273 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4274 []
4275 )
4276 });
4277
4278 project_c.read_with(cx_c, |project, cx| {
4279 assert_eq!(
4280 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4281 []
4282 )
4283 });
4284}
4285
4286#[gpui::test(iterations = 10)]
4287async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
4288 executor: BackgroundExecutor,
4289 cx_a: &mut TestAppContext,
4290 cx_b: &mut TestAppContext,
4291) {
4292 let mut server = TestServer::start(executor.clone()).await;
4293 let client_a = server.create_client(cx_a, "user_a").await;
4294 let client_b = server.create_client(cx_b, "user_b").await;
4295 server
4296 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4297 .await;
4298
4299 client_a.language_registry().add(rust_lang());
4300 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4301 "Rust",
4302 FakeLspAdapter {
4303 disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
4304 disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
4305 ..Default::default()
4306 },
4307 );
4308
4309 let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
4310 client_a
4311 .fs()
4312 .insert_tree(
4313 path!("/test"),
4314 json!({
4315 "one.rs": "const ONE: usize = 1;",
4316 "two.rs": "const TWO: usize = 2;",
4317 "three.rs": "const THREE: usize = 3;",
4318 "four.rs": "const FOUR: usize = 3;",
4319 "five.rs": "const FIVE: usize = 3;",
4320 }),
4321 )
4322 .await;
4323
4324 let (project_a, worktree_id) = client_a.build_local_project(path!("/test"), cx_a).await;
4325
4326 // Share a project as client A
4327 let active_call_a = cx_a.read(ActiveCall::global);
4328 let project_id = active_call_a
4329 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4330 .await
4331 .unwrap();
4332
4333 // Join the project as client B and open all three files.
4334 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4335 let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
4336 project_b.update(cx_b, |p, cx| {
4337 p.open_buffer_with_lsp((worktree_id, file_name), cx)
4338 })
4339 }))
4340 .await
4341 .unwrap();
4342
4343 // Simulate a language server reporting errors for a file.
4344 let fake_language_server = fake_language_servers.next().await.unwrap();
4345 fake_language_server
4346 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
4347 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4348 })
4349 .await
4350 .into_response()
4351 .unwrap();
4352 fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
4353 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4354 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
4355 lsp::WorkDoneProgressBegin {
4356 title: "Progress Began".into(),
4357 ..Default::default()
4358 },
4359 )),
4360 });
4361 for file_name in file_names {
4362 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4363 &lsp::PublishDiagnosticsParams {
4364 uri: lsp::Url::from_file_path(Path::new(path!("/test")).join(file_name)).unwrap(),
4365 version: None,
4366 diagnostics: vec![lsp::Diagnostic {
4367 severity: Some(lsp::DiagnosticSeverity::WARNING),
4368 source: Some("the-disk-based-diagnostics-source".into()),
4369 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
4370 message: "message one".to_string(),
4371 ..Default::default()
4372 }],
4373 },
4374 );
4375 }
4376 fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
4377 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4378 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
4379 lsp::WorkDoneProgressEnd { message: None },
4380 )),
4381 });
4382
4383 // When the "disk base diagnostics finished" message is received, the buffers'
4384 // diagnostics are expected to be present.
4385 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
4386 project_b.update(cx_b, {
4387 let project_b = project_b.clone();
4388 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
4389 move |_, cx| {
4390 cx.subscribe(&project_b, move |_, _, event, cx| {
4391 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
4392 disk_based_diagnostics_finished.store(true, SeqCst);
4393 for (buffer, _) in &guest_buffers {
4394 assert_eq!(
4395 buffer
4396 .read(cx)
4397 .snapshot()
4398 .diagnostics_in_range::<_, usize>(0..5, false)
4399 .count(),
4400 1,
4401 "expected a diagnostic for buffer {:?}",
4402 buffer.read(cx).file().unwrap().path(),
4403 );
4404 }
4405 }
4406 })
4407 .detach();
4408 }
4409 });
4410
4411 executor.run_until_parked();
4412 assert!(disk_based_diagnostics_finished.load(SeqCst));
4413}
4414
4415#[gpui::test(iterations = 10)]
4416async fn test_reloading_buffer_manually(
4417 executor: BackgroundExecutor,
4418 cx_a: &mut TestAppContext,
4419 cx_b: &mut TestAppContext,
4420) {
4421 let mut server = TestServer::start(executor.clone()).await;
4422 let client_a = server.create_client(cx_a, "user_a").await;
4423 let client_b = server.create_client(cx_b, "user_b").await;
4424 server
4425 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4426 .await;
4427 let active_call_a = cx_a.read(ActiveCall::global);
4428
4429 client_a
4430 .fs()
4431 .insert_tree(path!("/a"), json!({ "a.rs": "let one = 1;" }))
4432 .await;
4433 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
4434 let buffer_a = project_a
4435 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4436 .await
4437 .unwrap();
4438 let project_id = active_call_a
4439 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4440 .await
4441 .unwrap();
4442
4443 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4444
4445 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4446 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4447 buffer_b.update(cx_b, |buffer, cx| {
4448 buffer.edit([(4..7, "six")], None, cx);
4449 buffer.edit([(10..11, "6")], None, cx);
4450 assert_eq!(buffer.text(), "let six = 6;");
4451 assert!(buffer.is_dirty());
4452 assert!(!buffer.has_conflict());
4453 });
4454 executor.run_until_parked();
4455
4456 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4457
4458 client_a
4459 .fs()
4460 .save(
4461 path!("/a/a.rs").as_ref(),
4462 &Rope::from("let seven = 7;"),
4463 LineEnding::Unix,
4464 )
4465 .await
4466 .unwrap();
4467 executor.run_until_parked();
4468
4469 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4470
4471 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4472
4473 project_b
4474 .update(cx_b, |project, cx| {
4475 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4476 })
4477 .await
4478 .unwrap();
4479
4480 buffer_a.read_with(cx_a, |buffer, _| {
4481 assert_eq!(buffer.text(), "let seven = 7;");
4482 assert!(!buffer.is_dirty());
4483 assert!(!buffer.has_conflict());
4484 });
4485
4486 buffer_b.read_with(cx_b, |buffer, _| {
4487 assert_eq!(buffer.text(), "let seven = 7;");
4488 assert!(!buffer.is_dirty());
4489 assert!(!buffer.has_conflict());
4490 });
4491
4492 buffer_a.update(cx_a, |buffer, cx| {
4493 // Undoing on the host is a no-op when the reload was initiated by the guest.
4494 buffer.undo(cx);
4495 assert_eq!(buffer.text(), "let seven = 7;");
4496 assert!(!buffer.is_dirty());
4497 assert!(!buffer.has_conflict());
4498 });
4499 buffer_b.update(cx_b, |buffer, cx| {
4500 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4501 buffer.undo(cx);
4502 assert_eq!(buffer.text(), "let six = 6;");
4503 assert!(buffer.is_dirty());
4504 assert!(!buffer.has_conflict());
4505 });
4506}
4507
4508#[gpui::test(iterations = 10)]
4509async fn test_formatting_buffer(
4510 executor: BackgroundExecutor,
4511 cx_a: &mut TestAppContext,
4512 cx_b: &mut TestAppContext,
4513) {
4514 let mut server = TestServer::start(executor.clone()).await;
4515 let client_a = server.create_client(cx_a, "user_a").await;
4516 let client_b = server.create_client(cx_b, "user_b").await;
4517 server
4518 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4519 .await;
4520 let active_call_a = cx_a.read(ActiveCall::global);
4521
4522 client_a.language_registry().add(rust_lang());
4523 let mut fake_language_servers = client_a
4524 .language_registry()
4525 .register_fake_lsp("Rust", FakeLspAdapter::default());
4526
4527 // Here we insert a fake tree with a directory that exists on disk. This is needed
4528 // because later we'll invoke a command, which requires passing a working directory
4529 // that points to a valid location on disk.
4530 let directory = env::current_dir().unwrap();
4531 client_a
4532 .fs()
4533 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4534 .await;
4535 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4536 let project_id = active_call_a
4537 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4538 .await
4539 .unwrap();
4540 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4541
4542 let buffer_b = project_b
4543 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4544 .await
4545 .unwrap();
4546
4547 let _handle = project_b.update(cx_b, |project, cx| {
4548 project.register_buffer_with_language_servers(&buffer_b, cx)
4549 });
4550 let fake_language_server = fake_language_servers.next().await.unwrap();
4551 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(|_, _| async move {
4552 Ok(Some(vec![
4553 lsp::TextEdit {
4554 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4555 new_text: "h".to_string(),
4556 },
4557 lsp::TextEdit {
4558 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4559 new_text: "y".to_string(),
4560 },
4561 ]))
4562 });
4563
4564 project_b
4565 .update(cx_b, |project, cx| {
4566 project.format(
4567 HashSet::from_iter([buffer_b.clone()]),
4568 LspFormatTarget::Buffers,
4569 true,
4570 FormatTrigger::Save,
4571 cx,
4572 )
4573 })
4574 .await
4575 .unwrap();
4576
4577 // The edits from the LSP are applied, and a final newline is added.
4578 assert_eq!(
4579 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4580 "let honey = \"two\"\n"
4581 );
4582
4583 // There is no `awk` command on Windows.
4584 #[cfg(not(target_os = "windows"))]
4585 {
4586 // Ensure buffer can be formatted using an external command. Notice how the
4587 // host's configuration is honored as opposed to using the guest's settings.
4588 cx_a.update(|cx| {
4589 SettingsStore::update_global(cx, |store, cx| {
4590 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4591 file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
4592 vec![Formatter::External {
4593 command: "awk".into(),
4594 arguments: Some(
4595 vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
4596 ),
4597 }]
4598 .into(),
4599 )));
4600 });
4601 });
4602 });
4603
4604 executor.allow_parking();
4605 project_b
4606 .update(cx_b, |project, cx| {
4607 project.format(
4608 HashSet::from_iter([buffer_b.clone()]),
4609 LspFormatTarget::Buffers,
4610 true,
4611 FormatTrigger::Save,
4612 cx,
4613 )
4614 })
4615 .await
4616 .unwrap();
4617 assert_eq!(
4618 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4619 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4620 );
4621 }
4622}
4623
4624#[gpui::test(iterations = 10)]
4625async fn test_prettier_formatting_buffer(
4626 executor: BackgroundExecutor,
4627 cx_a: &mut TestAppContext,
4628 cx_b: &mut TestAppContext,
4629) {
4630 let mut server = TestServer::start(executor.clone()).await;
4631 let client_a = server.create_client(cx_a, "user_a").await;
4632 let client_b = server.create_client(cx_b, "user_b").await;
4633 server
4634 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4635 .await;
4636 let active_call_a = cx_a.read(ActiveCall::global);
4637
4638 let test_plugin = "test_plugin";
4639
4640 client_a.language_registry().add(Arc::new(Language::new(
4641 LanguageConfig {
4642 name: "TypeScript".into(),
4643 matcher: LanguageMatcher {
4644 path_suffixes: vec!["ts".to_string()],
4645 ..Default::default()
4646 },
4647 ..Default::default()
4648 },
4649 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
4650 )));
4651 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4652 "TypeScript",
4653 FakeLspAdapter {
4654 prettier_plugins: vec![test_plugin],
4655 ..Default::default()
4656 },
4657 );
4658
4659 // Here we insert a fake tree with a directory that exists on disk. This is needed
4660 // because later we'll invoke a command, which requires passing a working directory
4661 // that points to a valid location on disk.
4662 let directory = env::current_dir().unwrap();
4663 let buffer_text = "let one = \"two\"";
4664 client_a
4665 .fs()
4666 .insert_tree(&directory, json!({ "a.ts": buffer_text }))
4667 .await;
4668 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4669 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
4670 let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
4671 let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
4672
4673 let project_id = active_call_a
4674 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4675 .await
4676 .unwrap();
4677 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4678 let (buffer_b, _) = project_b
4679 .update(cx_b, |p, cx| {
4680 p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
4681 })
4682 .await
4683 .unwrap();
4684
4685 cx_a.update(|cx| {
4686 SettingsStore::update_global(cx, |store, cx| {
4687 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4688 file.defaults.formatter = Some(SelectedFormatter::Auto);
4689 file.defaults.prettier = Some(PrettierSettings {
4690 allowed: true,
4691 ..PrettierSettings::default()
4692 });
4693 });
4694 });
4695 });
4696 cx_b.update(|cx| {
4697 SettingsStore::update_global(cx, |store, cx| {
4698 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4699 file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
4700 vec![Formatter::LanguageServer { name: None }].into(),
4701 )));
4702 file.defaults.prettier = Some(PrettierSettings {
4703 allowed: true,
4704 ..PrettierSettings::default()
4705 });
4706 });
4707 });
4708 });
4709 let fake_language_server = fake_language_servers.next().await.unwrap();
4710 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(|_, _| async move {
4711 panic!(
4712 "Unexpected: prettier should be preferred since it's enabled and language supports it"
4713 )
4714 });
4715
4716 project_b
4717 .update(cx_b, |project, cx| {
4718 project.format(
4719 HashSet::from_iter([buffer_b.clone()]),
4720 LspFormatTarget::Buffers,
4721 true,
4722 FormatTrigger::Save,
4723 cx,
4724 )
4725 })
4726 .await
4727 .unwrap();
4728
4729 executor.run_until_parked();
4730 assert_eq!(
4731 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4732 buffer_text.to_string() + "\n" + prettier_format_suffix,
4733 "Prettier formatting was not applied to client buffer after client's request"
4734 );
4735
4736 project_a
4737 .update(cx_a, |project, cx| {
4738 project.format(
4739 HashSet::from_iter([buffer_a.clone()]),
4740 LspFormatTarget::Buffers,
4741 true,
4742 FormatTrigger::Manual,
4743 cx,
4744 )
4745 })
4746 .await
4747 .unwrap();
4748
4749 executor.run_until_parked();
4750 assert_eq!(
4751 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4752 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
4753 "Prettier formatting was not applied to client buffer after host's request"
4754 );
4755}
4756
4757#[gpui::test(iterations = 10)]
4758async fn test_definition(
4759 executor: BackgroundExecutor,
4760 cx_a: &mut TestAppContext,
4761 cx_b: &mut TestAppContext,
4762) {
4763 let mut server = TestServer::start(executor.clone()).await;
4764 let client_a = server.create_client(cx_a, "user_a").await;
4765 let client_b = server.create_client(cx_b, "user_b").await;
4766 server
4767 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4768 .await;
4769 let active_call_a = cx_a.read(ActiveCall::global);
4770
4771 let mut fake_language_servers = client_a
4772 .language_registry()
4773 .register_fake_lsp("Rust", Default::default());
4774 client_a.language_registry().add(rust_lang());
4775
4776 client_a
4777 .fs()
4778 .insert_tree(
4779 path!("/root"),
4780 json!({
4781 "dir-1": {
4782 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4783 },
4784 "dir-2": {
4785 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4786 "c.rs": "type T2 = usize;",
4787 }
4788 }),
4789 )
4790 .await;
4791 let (project_a, worktree_id) = client_a
4792 .build_local_project(path!("/root/dir-1"), cx_a)
4793 .await;
4794 let project_id = active_call_a
4795 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4796 .await
4797 .unwrap();
4798 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4799
4800 // Open the file on client B.
4801 let (buffer_b, _handle) = project_b
4802 .update(cx_b, |p, cx| {
4803 p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
4804 })
4805 .await
4806 .unwrap();
4807
4808 // Request the definition of a symbol as the guest.
4809 let fake_language_server = fake_language_servers.next().await.unwrap();
4810 fake_language_server.set_request_handler::<lsp::request::GotoDefinition, _, _>(
4811 |_, _| async move {
4812 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4813 lsp::Location::new(
4814 lsp::Url::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
4815 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4816 ),
4817 )))
4818 },
4819 );
4820
4821 let definitions_1 = project_b
4822 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
4823 .await
4824 .unwrap();
4825 cx_b.read(|cx| {
4826 assert_eq!(definitions_1.len(), 1);
4827 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4828 let target_buffer = definitions_1[0].target.buffer.read(cx);
4829 assert_eq!(
4830 target_buffer.text(),
4831 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4832 );
4833 assert_eq!(
4834 definitions_1[0].target.range.to_point(target_buffer),
4835 Point::new(0, 6)..Point::new(0, 9)
4836 );
4837 });
4838
4839 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4840 // the previous call to `definition`.
4841 fake_language_server.set_request_handler::<lsp::request::GotoDefinition, _, _>(
4842 |_, _| async move {
4843 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4844 lsp::Location::new(
4845 lsp::Url::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
4846 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4847 ),
4848 )))
4849 },
4850 );
4851
4852 let definitions_2 = project_b
4853 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4854 .await
4855 .unwrap();
4856 cx_b.read(|cx| {
4857 assert_eq!(definitions_2.len(), 1);
4858 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4859 let target_buffer = definitions_2[0].target.buffer.read(cx);
4860 assert_eq!(
4861 target_buffer.text(),
4862 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4863 );
4864 assert_eq!(
4865 definitions_2[0].target.range.to_point(target_buffer),
4866 Point::new(1, 6)..Point::new(1, 11)
4867 );
4868 });
4869 assert_eq!(
4870 definitions_1[0].target.buffer,
4871 definitions_2[0].target.buffer
4872 );
4873
4874 fake_language_server.set_request_handler::<lsp::request::GotoTypeDefinition, _, _>(
4875 |req, _| async move {
4876 assert_eq!(
4877 req.text_document_position_params.position,
4878 lsp::Position::new(0, 7)
4879 );
4880 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4881 lsp::Location::new(
4882 lsp::Url::from_file_path(path!("/root/dir-2/c.rs")).unwrap(),
4883 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4884 ),
4885 )))
4886 },
4887 );
4888
4889 let type_definitions = project_b
4890 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4891 .await
4892 .unwrap();
4893 cx_b.read(|cx| {
4894 assert_eq!(type_definitions.len(), 1);
4895 let target_buffer = type_definitions[0].target.buffer.read(cx);
4896 assert_eq!(target_buffer.text(), "type T2 = usize;");
4897 assert_eq!(
4898 type_definitions[0].target.range.to_point(target_buffer),
4899 Point::new(0, 5)..Point::new(0, 7)
4900 );
4901 });
4902}
4903
4904#[gpui::test(iterations = 10)]
4905async fn test_references(
4906 executor: BackgroundExecutor,
4907 cx_a: &mut TestAppContext,
4908 cx_b: &mut TestAppContext,
4909) {
4910 let mut server = TestServer::start(executor.clone()).await;
4911 let client_a = server.create_client(cx_a, "user_a").await;
4912 let client_b = server.create_client(cx_b, "user_b").await;
4913 server
4914 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4915 .await;
4916 let active_call_a = cx_a.read(ActiveCall::global);
4917
4918 client_a.language_registry().add(rust_lang());
4919 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4920 "Rust",
4921 FakeLspAdapter {
4922 name: "my-fake-lsp-adapter",
4923 capabilities: lsp::ServerCapabilities {
4924 references_provider: Some(lsp::OneOf::Left(true)),
4925 ..Default::default()
4926 },
4927 ..Default::default()
4928 },
4929 );
4930
4931 client_a
4932 .fs()
4933 .insert_tree(
4934 path!("/root"),
4935 json!({
4936 "dir-1": {
4937 "one.rs": "const ONE: usize = 1;",
4938 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4939 },
4940 "dir-2": {
4941 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4942 }
4943 }),
4944 )
4945 .await;
4946 let (project_a, worktree_id) = client_a
4947 .build_local_project(path!("/root/dir-1"), cx_a)
4948 .await;
4949 let project_id = active_call_a
4950 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4951 .await
4952 .unwrap();
4953 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4954
4955 // Open the file on client B.
4956 let (buffer_b, _handle) = project_b
4957 .update(cx_b, |p, cx| {
4958 p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
4959 })
4960 .await
4961 .unwrap();
4962
4963 // Request references to a symbol as the guest.
4964 let fake_language_server = fake_language_servers.next().await.unwrap();
4965 let (lsp_response_tx, rx) = mpsc::unbounded::<Result<Option<Vec<lsp::Location>>>>();
4966 fake_language_server.set_request_handler::<lsp::request::References, _, _>({
4967 let rx = Arc::new(Mutex::new(Some(rx)));
4968 move |params, _| {
4969 assert_eq!(
4970 params.text_document_position.text_document.uri.as_str(),
4971 uri!("file:///root/dir-1/one.rs")
4972 );
4973 let rx = rx.clone();
4974 async move {
4975 let mut response_rx = rx.lock().take().unwrap();
4976 let result = response_rx.next().await.unwrap();
4977 *rx.lock() = Some(response_rx);
4978 result
4979 }
4980 }
4981 });
4982
4983 let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx));
4984
4985 // User is informed that a request is pending.
4986 executor.run_until_parked();
4987 project_b.read_with(cx_b, |project, cx| {
4988 let status = project.language_server_statuses(cx).next().unwrap().1;
4989 assert_eq!(status.name, "my-fake-lsp-adapter");
4990 assert_eq!(
4991 status.pending_work.values().next().unwrap().message,
4992 Some("Finding references...".into())
4993 );
4994 });
4995
4996 // Cause the language server to respond.
4997 lsp_response_tx
4998 .unbounded_send(Ok(Some(vec![
4999 lsp::Location {
5000 uri: lsp::Url::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
5001 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
5002 },
5003 lsp::Location {
5004 uri: lsp::Url::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
5005 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
5006 },
5007 lsp::Location {
5008 uri: lsp::Url::from_file_path(path!("/root/dir-2/three.rs")).unwrap(),
5009 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
5010 },
5011 ])))
5012 .unwrap();
5013
5014 let references = references.await.unwrap();
5015 executor.run_until_parked();
5016 project_b.read_with(cx_b, |project, cx| {
5017 // User is informed that a request is no longer pending.
5018 let status = project.language_server_statuses(cx).next().unwrap().1;
5019 assert!(status.pending_work.is_empty());
5020
5021 assert_eq!(references.len(), 3);
5022 assert_eq!(project.worktrees(cx).count(), 2);
5023
5024 let two_buffer = references[0].buffer.read(cx);
5025 let three_buffer = references[2].buffer.read(cx);
5026 assert_eq!(
5027 two_buffer.file().unwrap().path().as_ref(),
5028 Path::new("two.rs")
5029 );
5030 assert_eq!(references[1].buffer, references[0].buffer);
5031 assert_eq!(
5032 three_buffer.file().unwrap().full_path(cx),
5033 Path::new(path!("/root/dir-2/three.rs"))
5034 );
5035
5036 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
5037 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
5038 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
5039 });
5040
5041 let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx));
5042
5043 // User is informed that a request is pending.
5044 executor.run_until_parked();
5045 project_b.read_with(cx_b, |project, cx| {
5046 let status = project.language_server_statuses(cx).next().unwrap().1;
5047 assert_eq!(status.name, "my-fake-lsp-adapter");
5048 assert_eq!(
5049 status.pending_work.values().next().unwrap().message,
5050 Some("Finding references...".into())
5051 );
5052 });
5053
5054 // Cause the LSP request to fail.
5055 lsp_response_tx
5056 .unbounded_send(Err(anyhow!("can't find references")))
5057 .unwrap();
5058 references.await.unwrap_err();
5059
5060 // User is informed that the request is no longer pending.
5061 executor.run_until_parked();
5062 project_b.read_with(cx_b, |project, cx| {
5063 let status = project.language_server_statuses(cx).next().unwrap().1;
5064 assert!(status.pending_work.is_empty());
5065 });
5066}
5067
5068#[gpui::test(iterations = 10)]
5069async fn test_project_search(
5070 executor: BackgroundExecutor,
5071 cx_a: &mut TestAppContext,
5072 cx_b: &mut TestAppContext,
5073) {
5074 let mut server = TestServer::start(executor.clone()).await;
5075 let client_a = server.create_client(cx_a, "user_a").await;
5076 let client_b = server.create_client(cx_b, "user_b").await;
5077 server
5078 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5079 .await;
5080 let active_call_a = cx_a.read(ActiveCall::global);
5081
5082 client_a
5083 .fs()
5084 .insert_tree(
5085 "/root",
5086 json!({
5087 "dir-1": {
5088 "a": "hello world",
5089 "b": "goodnight moon",
5090 "c": "a world of goo",
5091 "d": "world champion of clown world",
5092 },
5093 "dir-2": {
5094 "e": "disney world is fun",
5095 }
5096 }),
5097 )
5098 .await;
5099 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
5100 let (worktree_2, _) = project_a
5101 .update(cx_a, |p, cx| {
5102 p.find_or_create_worktree("/root/dir-2", true, cx)
5103 })
5104 .await
5105 .unwrap();
5106 worktree_2
5107 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
5108 .await;
5109 let project_id = active_call_a
5110 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5111 .await
5112 .unwrap();
5113
5114 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5115
5116 // Perform a search as the guest.
5117 let mut results = HashMap::default();
5118 let search_rx = project_b.update(cx_b, |project, cx| {
5119 project.search(
5120 SearchQuery::text(
5121 "world",
5122 false,
5123 false,
5124 false,
5125 Default::default(),
5126 Default::default(),
5127 false,
5128 None,
5129 )
5130 .unwrap(),
5131 cx,
5132 )
5133 });
5134 while let Ok(result) = search_rx.recv().await {
5135 match result {
5136 SearchResult::Buffer { buffer, ranges } => {
5137 results.entry(buffer).or_insert(ranges);
5138 }
5139 SearchResult::LimitReached => {
5140 panic!(
5141 "Unexpectedly reached search limit in tests. If you do want to assert limit-reached, change this panic call."
5142 )
5143 }
5144 };
5145 }
5146
5147 let mut ranges_by_path = results
5148 .into_iter()
5149 .map(|(buffer, ranges)| {
5150 buffer.read_with(cx_b, |buffer, cx| {
5151 let path = buffer.file().unwrap().full_path(cx);
5152 let offset_ranges = ranges
5153 .into_iter()
5154 .map(|range| range.to_offset(buffer))
5155 .collect::<Vec<_>>();
5156 (path, offset_ranges)
5157 })
5158 })
5159 .collect::<Vec<_>>();
5160 ranges_by_path.sort_by_key(|(path, _)| path.clone());
5161
5162 assert_eq!(
5163 ranges_by_path,
5164 &[
5165 (PathBuf::from("dir-1/a"), vec![6..11]),
5166 (PathBuf::from("dir-1/c"), vec![2..7]),
5167 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
5168 (PathBuf::from("dir-2/e"), vec![7..12]),
5169 ]
5170 );
5171}
5172
5173#[gpui::test(iterations = 10)]
5174async fn test_document_highlights(
5175 executor: BackgroundExecutor,
5176 cx_a: &mut TestAppContext,
5177 cx_b: &mut TestAppContext,
5178) {
5179 let mut server = TestServer::start(executor.clone()).await;
5180 let client_a = server.create_client(cx_a, "user_a").await;
5181 let client_b = server.create_client(cx_b, "user_b").await;
5182 server
5183 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5184 .await;
5185 let active_call_a = cx_a.read(ActiveCall::global);
5186
5187 client_a
5188 .fs()
5189 .insert_tree(
5190 path!("/root-1"),
5191 json!({
5192 "main.rs": "fn double(number: i32) -> i32 { number + number }",
5193 }),
5194 )
5195 .await;
5196
5197 let mut fake_language_servers = client_a
5198 .language_registry()
5199 .register_fake_lsp("Rust", Default::default());
5200 client_a.language_registry().add(rust_lang());
5201
5202 let (project_a, worktree_id) = client_a.build_local_project(path!("/root-1"), cx_a).await;
5203 let project_id = active_call_a
5204 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5205 .await
5206 .unwrap();
5207 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5208
5209 // Open the file on client B.
5210 let (buffer_b, _handle) = project_b
5211 .update(cx_b, |p, cx| {
5212 p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
5213 })
5214 .await
5215 .unwrap();
5216
5217 // Request document highlights as the guest.
5218 let fake_language_server = fake_language_servers.next().await.unwrap();
5219 fake_language_server.set_request_handler::<lsp::request::DocumentHighlightRequest, _, _>(
5220 |params, _| async move {
5221 assert_eq!(
5222 params
5223 .text_document_position_params
5224 .text_document
5225 .uri
5226 .as_str(),
5227 uri!("file:///root-1/main.rs")
5228 );
5229 assert_eq!(
5230 params.text_document_position_params.position,
5231 lsp::Position::new(0, 34)
5232 );
5233 Ok(Some(vec![
5234 lsp::DocumentHighlight {
5235 kind: Some(lsp::DocumentHighlightKind::WRITE),
5236 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
5237 },
5238 lsp::DocumentHighlight {
5239 kind: Some(lsp::DocumentHighlightKind::READ),
5240 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
5241 },
5242 lsp::DocumentHighlight {
5243 kind: Some(lsp::DocumentHighlightKind::READ),
5244 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
5245 },
5246 ]))
5247 },
5248 );
5249
5250 let highlights = project_b
5251 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
5252 .await
5253 .unwrap();
5254
5255 buffer_b.read_with(cx_b, |buffer, _| {
5256 let snapshot = buffer.snapshot();
5257
5258 let highlights = highlights
5259 .into_iter()
5260 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
5261 .collect::<Vec<_>>();
5262 assert_eq!(
5263 highlights,
5264 &[
5265 (lsp::DocumentHighlightKind::WRITE, 10..16),
5266 (lsp::DocumentHighlightKind::READ, 32..38),
5267 (lsp::DocumentHighlightKind::READ, 41..47)
5268 ]
5269 )
5270 });
5271}
5272
5273#[gpui::test(iterations = 10)]
5274async fn test_lsp_hover(
5275 executor: BackgroundExecutor,
5276 cx_a: &mut TestAppContext,
5277 cx_b: &mut TestAppContext,
5278) {
5279 let mut server = TestServer::start(executor.clone()).await;
5280 let client_a = server.create_client(cx_a, "user_a").await;
5281 let client_b = server.create_client(cx_b, "user_b").await;
5282 server
5283 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5284 .await;
5285 let active_call_a = cx_a.read(ActiveCall::global);
5286
5287 client_a
5288 .fs()
5289 .insert_tree(
5290 path!("/root-1"),
5291 json!({
5292 "main.rs": "use std::collections::HashMap;",
5293 }),
5294 )
5295 .await;
5296
5297 client_a.language_registry().add(rust_lang());
5298 let language_server_names = ["rust-analyzer", "CrabLang-ls"];
5299 let mut language_servers = [
5300 client_a.language_registry().register_fake_lsp(
5301 "Rust",
5302 FakeLspAdapter {
5303 name: "rust-analyzer",
5304 capabilities: lsp::ServerCapabilities {
5305 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5306 ..lsp::ServerCapabilities::default()
5307 },
5308 ..FakeLspAdapter::default()
5309 },
5310 ),
5311 client_a.language_registry().register_fake_lsp(
5312 "Rust",
5313 FakeLspAdapter {
5314 name: "CrabLang-ls",
5315 capabilities: lsp::ServerCapabilities {
5316 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5317 ..lsp::ServerCapabilities::default()
5318 },
5319 ..FakeLspAdapter::default()
5320 },
5321 ),
5322 ];
5323
5324 let (project_a, worktree_id) = client_a.build_local_project(path!("/root-1"), cx_a).await;
5325 let project_id = active_call_a
5326 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5327 .await
5328 .unwrap();
5329 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5330
5331 // Open the file as the guest
5332 let (buffer_b, _handle) = project_b
5333 .update(cx_b, |p, cx| {
5334 p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
5335 })
5336 .await
5337 .unwrap();
5338
5339 let mut servers_with_hover_requests = HashMap::default();
5340 for i in 0..language_server_names.len() {
5341 let new_server = language_servers[i].next().await.unwrap_or_else(|| {
5342 panic!(
5343 "Failed to get language server #{i} with name {}",
5344 &language_server_names[i]
5345 )
5346 });
5347 let new_server_name = new_server.server.name();
5348 assert!(
5349 !servers_with_hover_requests.contains_key(&new_server_name),
5350 "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
5351 );
5352 match new_server_name.as_ref() {
5353 "CrabLang-ls" => {
5354 servers_with_hover_requests.insert(
5355 new_server_name.clone(),
5356 new_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
5357 move |params, _| {
5358 assert_eq!(
5359 params
5360 .text_document_position_params
5361 .text_document
5362 .uri
5363 .as_str(),
5364 uri!("file:///root-1/main.rs")
5365 );
5366 let name = new_server_name.clone();
5367 async move {
5368 Ok(Some(lsp::Hover {
5369 contents: lsp::HoverContents::Scalar(
5370 lsp::MarkedString::String(format!("{name} hover")),
5371 ),
5372 range: None,
5373 }))
5374 }
5375 },
5376 ),
5377 );
5378 }
5379 "rust-analyzer" => {
5380 servers_with_hover_requests.insert(
5381 new_server_name.clone(),
5382 new_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
5383 |params, _| async move {
5384 assert_eq!(
5385 params
5386 .text_document_position_params
5387 .text_document
5388 .uri
5389 .as_str(),
5390 uri!("file:///root-1/main.rs")
5391 );
5392 assert_eq!(
5393 params.text_document_position_params.position,
5394 lsp::Position::new(0, 22)
5395 );
5396 Ok(Some(lsp::Hover {
5397 contents: lsp::HoverContents::Array(vec![
5398 lsp::MarkedString::String("Test hover content.".to_string()),
5399 lsp::MarkedString::LanguageString(lsp::LanguageString {
5400 language: "Rust".to_string(),
5401 value: "let foo = 42;".to_string(),
5402 }),
5403 ]),
5404 range: Some(lsp::Range::new(
5405 lsp::Position::new(0, 22),
5406 lsp::Position::new(0, 29),
5407 )),
5408 }))
5409 },
5410 ),
5411 );
5412 }
5413 unexpected => panic!("Unexpected server name: {unexpected}"),
5414 }
5415 }
5416
5417 // Request hover information as the guest.
5418 let mut hovers = project_b
5419 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
5420 .await;
5421 assert_eq!(
5422 hovers.len(),
5423 2,
5424 "Expected two hovers from both language servers, but got: {hovers:?}"
5425 );
5426
5427 let _: Vec<()> = futures::future::join_all(servers_with_hover_requests.into_values().map(
5428 |mut hover_request| async move {
5429 hover_request
5430 .next()
5431 .await
5432 .expect("All hover requests should have been triggered")
5433 },
5434 ))
5435 .await;
5436
5437 hovers.sort_by_key(|hover| hover.contents.len());
5438 let first_hover = hovers.first().cloned().unwrap();
5439 assert_eq!(
5440 first_hover.contents,
5441 vec![project::HoverBlock {
5442 text: "CrabLang-ls hover".to_string(),
5443 kind: HoverBlockKind::Markdown,
5444 },]
5445 );
5446 let second_hover = hovers.last().cloned().unwrap();
5447 assert_eq!(
5448 second_hover.contents,
5449 vec![
5450 project::HoverBlock {
5451 text: "Test hover content.".to_string(),
5452 kind: HoverBlockKind::Markdown,
5453 },
5454 project::HoverBlock {
5455 text: "let foo = 42;".to_string(),
5456 kind: HoverBlockKind::Code {
5457 language: "Rust".to_string()
5458 },
5459 }
5460 ]
5461 );
5462 buffer_b.read_with(cx_b, |buffer, _| {
5463 let snapshot = buffer.snapshot();
5464 assert_eq!(second_hover.range.unwrap().to_offset(&snapshot), 22..29);
5465 });
5466}
5467
5468#[gpui::test(iterations = 10)]
5469async fn test_project_symbols(
5470 executor: BackgroundExecutor,
5471 cx_a: &mut TestAppContext,
5472 cx_b: &mut TestAppContext,
5473) {
5474 let mut server = TestServer::start(executor.clone()).await;
5475 let client_a = server.create_client(cx_a, "user_a").await;
5476 let client_b = server.create_client(cx_b, "user_b").await;
5477 server
5478 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5479 .await;
5480 let active_call_a = cx_a.read(ActiveCall::global);
5481
5482 client_a.language_registry().add(rust_lang());
5483 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
5484 "Rust",
5485 FakeLspAdapter {
5486 capabilities: lsp::ServerCapabilities {
5487 workspace_symbol_provider: Some(OneOf::Left(true)),
5488 ..Default::default()
5489 },
5490 ..Default::default()
5491 },
5492 );
5493
5494 client_a
5495 .fs()
5496 .insert_tree(
5497 path!("/code"),
5498 json!({
5499 "crate-1": {
5500 "one.rs": "const ONE: usize = 1;",
5501 },
5502 "crate-2": {
5503 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
5504 },
5505 "private": {
5506 "passwords.txt": "the-password",
5507 }
5508 }),
5509 )
5510 .await;
5511 let (project_a, worktree_id) = client_a
5512 .build_local_project(path!("/code/crate-1"), cx_a)
5513 .await;
5514 let project_id = active_call_a
5515 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5516 .await
5517 .unwrap();
5518 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5519
5520 // Cause the language server to start.
5521 let _buffer = project_b
5522 .update(cx_b, |p, cx| {
5523 p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
5524 })
5525 .await
5526 .unwrap();
5527
5528 let fake_language_server = fake_language_servers.next().await.unwrap();
5529 fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
5530 |_, _| async move {
5531 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
5532 #[allow(deprecated)]
5533 lsp::SymbolInformation {
5534 name: "TWO".into(),
5535 location: lsp::Location {
5536 uri: lsp::Url::from_file_path(path!("/code/crate-2/two.rs")).unwrap(),
5537 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5538 },
5539 kind: lsp::SymbolKind::CONSTANT,
5540 tags: None,
5541 container_name: None,
5542 deprecated: None,
5543 },
5544 ])))
5545 },
5546 );
5547
5548 // Request the definition of a symbol as the guest.
5549 let symbols = project_b
5550 .update(cx_b, |p, cx| p.symbols("two", cx))
5551 .await
5552 .unwrap();
5553 assert_eq!(symbols.len(), 1);
5554 assert_eq!(symbols[0].name, "TWO");
5555
5556 // Open one of the returned symbols.
5557 let buffer_b_2 = project_b
5558 .update(cx_b, |project, cx| {
5559 project.open_buffer_for_symbol(&symbols[0], cx)
5560 })
5561 .await
5562 .unwrap();
5563
5564 buffer_b_2.read_with(cx_b, |buffer, cx| {
5565 assert_eq!(
5566 buffer.file().unwrap().full_path(cx),
5567 Path::new(path!("/code/crate-2/two.rs"))
5568 );
5569 });
5570
5571 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
5572 let mut fake_symbol = symbols[0].clone();
5573 fake_symbol.path.path = Path::new(path!("/code/secrets")).into();
5574 let error = project_b
5575 .update(cx_b, |project, cx| {
5576 project.open_buffer_for_symbol(&fake_symbol, cx)
5577 })
5578 .await
5579 .unwrap_err();
5580 assert!(error.to_string().contains("invalid symbol signature"));
5581}
5582
5583#[gpui::test(iterations = 10)]
5584async fn test_open_buffer_while_getting_definition_pointing_to_it(
5585 executor: BackgroundExecutor,
5586 cx_a: &mut TestAppContext,
5587 cx_b: &mut TestAppContext,
5588 mut rng: StdRng,
5589) {
5590 let mut server = TestServer::start(executor.clone()).await;
5591 let client_a = server.create_client(cx_a, "user_a").await;
5592 let client_b = server.create_client(cx_b, "user_b").await;
5593 server
5594 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5595 .await;
5596 let active_call_a = cx_a.read(ActiveCall::global);
5597
5598 client_a.language_registry().add(rust_lang());
5599 let mut fake_language_servers = client_a
5600 .language_registry()
5601 .register_fake_lsp("Rust", Default::default());
5602
5603 client_a
5604 .fs()
5605 .insert_tree(
5606 path!("/root"),
5607 json!({
5608 "a.rs": "const ONE: usize = b::TWO;",
5609 "b.rs": "const TWO: usize = 2",
5610 }),
5611 )
5612 .await;
5613 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
5614 let project_id = active_call_a
5615 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5616 .await
5617 .unwrap();
5618 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5619
5620 let (buffer_b1, _lsp) = project_b
5621 .update(cx_b, |p, cx| {
5622 p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
5623 })
5624 .await
5625 .unwrap();
5626
5627 let fake_language_server = fake_language_servers.next().await.unwrap();
5628 fake_language_server.set_request_handler::<lsp::request::GotoDefinition, _, _>(
5629 |_, _| async move {
5630 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
5631 lsp::Location::new(
5632 lsp::Url::from_file_path(path!("/root/b.rs")).unwrap(),
5633 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5634 ),
5635 )))
5636 },
5637 );
5638
5639 let definitions;
5640 let buffer_b2;
5641 if rng.r#gen() {
5642 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5643 (buffer_b2, _) = project_b
5644 .update(cx_b, |p, cx| {
5645 p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
5646 })
5647 .await
5648 .unwrap();
5649 } else {
5650 (buffer_b2, _) = project_b
5651 .update(cx_b, |p, cx| {
5652 p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
5653 })
5654 .await
5655 .unwrap();
5656 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5657 }
5658
5659 let definitions = definitions.await.unwrap();
5660 assert_eq!(definitions.len(), 1);
5661 assert_eq!(definitions[0].target.buffer, buffer_b2);
5662}
5663
5664#[gpui::test(iterations = 10)]
5665async fn test_contacts(
5666 executor: BackgroundExecutor,
5667 cx_a: &mut TestAppContext,
5668 cx_b: &mut TestAppContext,
5669 cx_c: &mut TestAppContext,
5670 cx_d: &mut TestAppContext,
5671) {
5672 let mut server = TestServer::start(executor.clone()).await;
5673 let client_a = server.create_client(cx_a, "user_a").await;
5674 let client_b = server.create_client(cx_b, "user_b").await;
5675 let client_c = server.create_client(cx_c, "user_c").await;
5676 let client_d = server.create_client(cx_d, "user_d").await;
5677 server
5678 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5679 .await;
5680 let active_call_a = cx_a.read(ActiveCall::global);
5681 let active_call_b = cx_b.read(ActiveCall::global);
5682 let active_call_c = cx_c.read(ActiveCall::global);
5683 let _active_call_d = cx_d.read(ActiveCall::global);
5684
5685 executor.run_until_parked();
5686 assert_eq!(
5687 contacts(&client_a, cx_a),
5688 [
5689 ("user_b".to_string(), "online", "free"),
5690 ("user_c".to_string(), "online", "free")
5691 ]
5692 );
5693 assert_eq!(
5694 contacts(&client_b, cx_b),
5695 [
5696 ("user_a".to_string(), "online", "free"),
5697 ("user_c".to_string(), "online", "free")
5698 ]
5699 );
5700 assert_eq!(
5701 contacts(&client_c, cx_c),
5702 [
5703 ("user_a".to_string(), "online", "free"),
5704 ("user_b".to_string(), "online", "free")
5705 ]
5706 );
5707 assert_eq!(contacts(&client_d, cx_d), []);
5708
5709 server.disconnect_client(client_c.peer_id().unwrap());
5710 server.forbid_connections();
5711 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5712 assert_eq!(
5713 contacts(&client_a, cx_a),
5714 [
5715 ("user_b".to_string(), "online", "free"),
5716 ("user_c".to_string(), "offline", "free")
5717 ]
5718 );
5719 assert_eq!(
5720 contacts(&client_b, cx_b),
5721 [
5722 ("user_a".to_string(), "online", "free"),
5723 ("user_c".to_string(), "offline", "free")
5724 ]
5725 );
5726 assert_eq!(contacts(&client_c, cx_c), []);
5727 assert_eq!(contacts(&client_d, cx_d), []);
5728
5729 server.allow_connections();
5730 client_c
5731 .authenticate_and_connect(false, &cx_c.to_async())
5732 .await
5733 .into_response()
5734 .unwrap();
5735
5736 executor.run_until_parked();
5737 assert_eq!(
5738 contacts(&client_a, cx_a),
5739 [
5740 ("user_b".to_string(), "online", "free"),
5741 ("user_c".to_string(), "online", "free")
5742 ]
5743 );
5744 assert_eq!(
5745 contacts(&client_b, cx_b),
5746 [
5747 ("user_a".to_string(), "online", "free"),
5748 ("user_c".to_string(), "online", "free")
5749 ]
5750 );
5751 assert_eq!(
5752 contacts(&client_c, cx_c),
5753 [
5754 ("user_a".to_string(), "online", "free"),
5755 ("user_b".to_string(), "online", "free")
5756 ]
5757 );
5758 assert_eq!(contacts(&client_d, cx_d), []);
5759
5760 active_call_a
5761 .update(cx_a, |call, cx| {
5762 call.invite(client_b.user_id().unwrap(), None, cx)
5763 })
5764 .await
5765 .unwrap();
5766 executor.run_until_parked();
5767 assert_eq!(
5768 contacts(&client_a, cx_a),
5769 [
5770 ("user_b".to_string(), "online", "busy"),
5771 ("user_c".to_string(), "online", "free")
5772 ]
5773 );
5774 assert_eq!(
5775 contacts(&client_b, cx_b),
5776 [
5777 ("user_a".to_string(), "online", "busy"),
5778 ("user_c".to_string(), "online", "free")
5779 ]
5780 );
5781 assert_eq!(
5782 contacts(&client_c, cx_c),
5783 [
5784 ("user_a".to_string(), "online", "busy"),
5785 ("user_b".to_string(), "online", "busy")
5786 ]
5787 );
5788 assert_eq!(contacts(&client_d, cx_d), []);
5789
5790 // Client B and client D become contacts while client B is being called.
5791 server
5792 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5793 .await;
5794 executor.run_until_parked();
5795 assert_eq!(
5796 contacts(&client_a, cx_a),
5797 [
5798 ("user_b".to_string(), "online", "busy"),
5799 ("user_c".to_string(), "online", "free")
5800 ]
5801 );
5802 assert_eq!(
5803 contacts(&client_b, cx_b),
5804 [
5805 ("user_a".to_string(), "online", "busy"),
5806 ("user_c".to_string(), "online", "free"),
5807 ("user_d".to_string(), "online", "free"),
5808 ]
5809 );
5810 assert_eq!(
5811 contacts(&client_c, cx_c),
5812 [
5813 ("user_a".to_string(), "online", "busy"),
5814 ("user_b".to_string(), "online", "busy")
5815 ]
5816 );
5817 assert_eq!(
5818 contacts(&client_d, cx_d),
5819 [("user_b".to_string(), "online", "busy")]
5820 );
5821
5822 active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
5823 executor.run_until_parked();
5824 assert_eq!(
5825 contacts(&client_a, cx_a),
5826 [
5827 ("user_b".to_string(), "online", "free"),
5828 ("user_c".to_string(), "online", "free")
5829 ]
5830 );
5831 assert_eq!(
5832 contacts(&client_b, cx_b),
5833 [
5834 ("user_a".to_string(), "online", "free"),
5835 ("user_c".to_string(), "online", "free"),
5836 ("user_d".to_string(), "online", "free")
5837 ]
5838 );
5839 assert_eq!(
5840 contacts(&client_c, cx_c),
5841 [
5842 ("user_a".to_string(), "online", "free"),
5843 ("user_b".to_string(), "online", "free")
5844 ]
5845 );
5846 assert_eq!(
5847 contacts(&client_d, cx_d),
5848 [("user_b".to_string(), "online", "free")]
5849 );
5850
5851 active_call_c
5852 .update(cx_c, |call, cx| {
5853 call.invite(client_a.user_id().unwrap(), None, cx)
5854 })
5855 .await
5856 .unwrap();
5857 executor.run_until_parked();
5858 assert_eq!(
5859 contacts(&client_a, cx_a),
5860 [
5861 ("user_b".to_string(), "online", "free"),
5862 ("user_c".to_string(), "online", "busy")
5863 ]
5864 );
5865 assert_eq!(
5866 contacts(&client_b, cx_b),
5867 [
5868 ("user_a".to_string(), "online", "busy"),
5869 ("user_c".to_string(), "online", "busy"),
5870 ("user_d".to_string(), "online", "free")
5871 ]
5872 );
5873 assert_eq!(
5874 contacts(&client_c, cx_c),
5875 [
5876 ("user_a".to_string(), "online", "busy"),
5877 ("user_b".to_string(), "online", "free")
5878 ]
5879 );
5880 assert_eq!(
5881 contacts(&client_d, cx_d),
5882 [("user_b".to_string(), "online", "free")]
5883 );
5884
5885 active_call_a
5886 .update(cx_a, |call, cx| call.accept_incoming(cx))
5887 .await
5888 .unwrap();
5889 executor.run_until_parked();
5890 assert_eq!(
5891 contacts(&client_a, cx_a),
5892 [
5893 ("user_b".to_string(), "online", "free"),
5894 ("user_c".to_string(), "online", "busy")
5895 ]
5896 );
5897 assert_eq!(
5898 contacts(&client_b, cx_b),
5899 [
5900 ("user_a".to_string(), "online", "busy"),
5901 ("user_c".to_string(), "online", "busy"),
5902 ("user_d".to_string(), "online", "free")
5903 ]
5904 );
5905 assert_eq!(
5906 contacts(&client_c, cx_c),
5907 [
5908 ("user_a".to_string(), "online", "busy"),
5909 ("user_b".to_string(), "online", "free")
5910 ]
5911 );
5912 assert_eq!(
5913 contacts(&client_d, cx_d),
5914 [("user_b".to_string(), "online", "free")]
5915 );
5916
5917 active_call_a
5918 .update(cx_a, |call, cx| {
5919 call.invite(client_b.user_id().unwrap(), None, cx)
5920 })
5921 .await
5922 .unwrap();
5923 executor.run_until_parked();
5924 assert_eq!(
5925 contacts(&client_a, cx_a),
5926 [
5927 ("user_b".to_string(), "online", "busy"),
5928 ("user_c".to_string(), "online", "busy")
5929 ]
5930 );
5931 assert_eq!(
5932 contacts(&client_b, cx_b),
5933 [
5934 ("user_a".to_string(), "online", "busy"),
5935 ("user_c".to_string(), "online", "busy"),
5936 ("user_d".to_string(), "online", "free")
5937 ]
5938 );
5939 assert_eq!(
5940 contacts(&client_c, cx_c),
5941 [
5942 ("user_a".to_string(), "online", "busy"),
5943 ("user_b".to_string(), "online", "busy")
5944 ]
5945 );
5946 assert_eq!(
5947 contacts(&client_d, cx_d),
5948 [("user_b".to_string(), "online", "busy")]
5949 );
5950
5951 active_call_a
5952 .update(cx_a, |call, cx| call.hang_up(cx))
5953 .await
5954 .unwrap();
5955 executor.run_until_parked();
5956 assert_eq!(
5957 contacts(&client_a, cx_a),
5958 [
5959 ("user_b".to_string(), "online", "free"),
5960 ("user_c".to_string(), "online", "free")
5961 ]
5962 );
5963 assert_eq!(
5964 contacts(&client_b, cx_b),
5965 [
5966 ("user_a".to_string(), "online", "free"),
5967 ("user_c".to_string(), "online", "free"),
5968 ("user_d".to_string(), "online", "free")
5969 ]
5970 );
5971 assert_eq!(
5972 contacts(&client_c, cx_c),
5973 [
5974 ("user_a".to_string(), "online", "free"),
5975 ("user_b".to_string(), "online", "free")
5976 ]
5977 );
5978 assert_eq!(
5979 contacts(&client_d, cx_d),
5980 [("user_b".to_string(), "online", "free")]
5981 );
5982
5983 active_call_a
5984 .update(cx_a, |call, cx| {
5985 call.invite(client_b.user_id().unwrap(), None, cx)
5986 })
5987 .await
5988 .unwrap();
5989 executor.run_until_parked();
5990 assert_eq!(
5991 contacts(&client_a, cx_a),
5992 [
5993 ("user_b".to_string(), "online", "busy"),
5994 ("user_c".to_string(), "online", "free")
5995 ]
5996 );
5997 assert_eq!(
5998 contacts(&client_b, cx_b),
5999 [
6000 ("user_a".to_string(), "online", "busy"),
6001 ("user_c".to_string(), "online", "free"),
6002 ("user_d".to_string(), "online", "free")
6003 ]
6004 );
6005 assert_eq!(
6006 contacts(&client_c, cx_c),
6007 [
6008 ("user_a".to_string(), "online", "busy"),
6009 ("user_b".to_string(), "online", "busy")
6010 ]
6011 );
6012 assert_eq!(
6013 contacts(&client_d, cx_d),
6014 [("user_b".to_string(), "online", "busy")]
6015 );
6016
6017 server.forbid_connections();
6018 server.disconnect_client(client_a.peer_id().unwrap());
6019 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
6020 assert_eq!(contacts(&client_a, cx_a), []);
6021 assert_eq!(
6022 contacts(&client_b, cx_b),
6023 [
6024 ("user_a".to_string(), "offline", "free"),
6025 ("user_c".to_string(), "online", "free"),
6026 ("user_d".to_string(), "online", "free")
6027 ]
6028 );
6029 assert_eq!(
6030 contacts(&client_c, cx_c),
6031 [
6032 ("user_a".to_string(), "offline", "free"),
6033 ("user_b".to_string(), "online", "free")
6034 ]
6035 );
6036 assert_eq!(
6037 contacts(&client_d, cx_d),
6038 [("user_b".to_string(), "online", "free")]
6039 );
6040
6041 // Test removing a contact
6042 client_b
6043 .user_store()
6044 .update(cx_b, |store, cx| {
6045 store.remove_contact(client_c.user_id().unwrap(), cx)
6046 })
6047 .await
6048 .unwrap();
6049 executor.run_until_parked();
6050 assert_eq!(
6051 contacts(&client_b, cx_b),
6052 [
6053 ("user_a".to_string(), "offline", "free"),
6054 ("user_d".to_string(), "online", "free")
6055 ]
6056 );
6057 assert_eq!(
6058 contacts(&client_c, cx_c),
6059 [("user_a".to_string(), "offline", "free"),]
6060 );
6061
6062 fn contacts(
6063 client: &TestClient,
6064 cx: &TestAppContext,
6065 ) -> Vec<(String, &'static str, &'static str)> {
6066 client.user_store().read_with(cx, |store, _| {
6067 store
6068 .contacts()
6069 .iter()
6070 .map(|contact| {
6071 (
6072 contact.user.github_login.clone(),
6073 if contact.online { "online" } else { "offline" },
6074 if contact.busy { "busy" } else { "free" },
6075 )
6076 })
6077 .collect()
6078 })
6079 }
6080}
6081
6082#[gpui::test(iterations = 10)]
6083async fn test_contact_requests(
6084 executor: BackgroundExecutor,
6085 cx_a: &mut TestAppContext,
6086 cx_a2: &mut TestAppContext,
6087 cx_b: &mut TestAppContext,
6088 cx_b2: &mut TestAppContext,
6089 cx_c: &mut TestAppContext,
6090 cx_c2: &mut TestAppContext,
6091) {
6092 // Connect to a server as 3 clients.
6093 let mut server = TestServer::start(executor.clone()).await;
6094 let client_a = server.create_client(cx_a, "user_a").await;
6095 let client_a2 = server.create_client(cx_a2, "user_a").await;
6096 let client_b = server.create_client(cx_b, "user_b").await;
6097 let client_b2 = server.create_client(cx_b2, "user_b").await;
6098 let client_c = server.create_client(cx_c, "user_c").await;
6099 let client_c2 = server.create_client(cx_c2, "user_c").await;
6100
6101 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
6102 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
6103 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
6104
6105 // User A and User C request that user B become their contact.
6106 client_a
6107 .user_store()
6108 .update(cx_a, |store, cx| {
6109 store.request_contact(client_b.user_id().unwrap(), cx)
6110 })
6111 .await
6112 .unwrap();
6113 client_c
6114 .user_store()
6115 .update(cx_c, |store, cx| {
6116 store.request_contact(client_b.user_id().unwrap(), cx)
6117 })
6118 .await
6119 .unwrap();
6120 executor.run_until_parked();
6121
6122 // All users see the pending request appear in all their clients.
6123 assert_eq!(
6124 client_a.summarize_contacts(cx_a).outgoing_requests,
6125 &["user_b"]
6126 );
6127 assert_eq!(
6128 client_a2.summarize_contacts(cx_a2).outgoing_requests,
6129 &["user_b"]
6130 );
6131 assert_eq!(
6132 client_b.summarize_contacts(cx_b).incoming_requests,
6133 &["user_a", "user_c"]
6134 );
6135 assert_eq!(
6136 client_b2.summarize_contacts(cx_b2).incoming_requests,
6137 &["user_a", "user_c"]
6138 );
6139 assert_eq!(
6140 client_c.summarize_contacts(cx_c).outgoing_requests,
6141 &["user_b"]
6142 );
6143 assert_eq!(
6144 client_c2.summarize_contacts(cx_c2).outgoing_requests,
6145 &["user_b"]
6146 );
6147
6148 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
6149 disconnect_and_reconnect(&client_a, cx_a).await;
6150 disconnect_and_reconnect(&client_b, cx_b).await;
6151 disconnect_and_reconnect(&client_c, cx_c).await;
6152 executor.run_until_parked();
6153 assert_eq!(
6154 client_a.summarize_contacts(cx_a).outgoing_requests,
6155 &["user_b"]
6156 );
6157 assert_eq!(
6158 client_b.summarize_contacts(cx_b).incoming_requests,
6159 &["user_a", "user_c"]
6160 );
6161 assert_eq!(
6162 client_c.summarize_contacts(cx_c).outgoing_requests,
6163 &["user_b"]
6164 );
6165
6166 // User B accepts the request from user A.
6167 client_b
6168 .user_store()
6169 .update(cx_b, |store, cx| {
6170 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
6171 })
6172 .await
6173 .unwrap();
6174
6175 executor.run_until_parked();
6176
6177 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
6178 let contacts_b = client_b.summarize_contacts(cx_b);
6179 assert_eq!(contacts_b.current, &["user_a"]);
6180 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
6181 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6182 assert_eq!(contacts_b2.current, &["user_a"]);
6183 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
6184
6185 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
6186 let contacts_a = client_a.summarize_contacts(cx_a);
6187 assert_eq!(contacts_a.current, &["user_b"]);
6188 assert!(contacts_a.outgoing_requests.is_empty());
6189 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
6190 assert_eq!(contacts_a2.current, &["user_b"]);
6191 assert!(contacts_a2.outgoing_requests.is_empty());
6192
6193 // Contacts are present upon connecting (tested here via disconnect/reconnect)
6194 disconnect_and_reconnect(&client_a, cx_a).await;
6195 disconnect_and_reconnect(&client_b, cx_b).await;
6196 disconnect_and_reconnect(&client_c, cx_c).await;
6197 executor.run_until_parked();
6198 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6199 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6200 assert_eq!(
6201 client_b.summarize_contacts(cx_b).incoming_requests,
6202 &["user_c"]
6203 );
6204 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6205 assert_eq!(
6206 client_c.summarize_contacts(cx_c).outgoing_requests,
6207 &["user_b"]
6208 );
6209
6210 // User B rejects the request from user C.
6211 client_b
6212 .user_store()
6213 .update(cx_b, |store, cx| {
6214 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
6215 })
6216 .await
6217 .unwrap();
6218
6219 executor.run_until_parked();
6220
6221 // User B doesn't see user C as their contact, and the incoming request from them is removed.
6222 let contacts_b = client_b.summarize_contacts(cx_b);
6223 assert_eq!(contacts_b.current, &["user_a"]);
6224 assert!(contacts_b.incoming_requests.is_empty());
6225 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6226 assert_eq!(contacts_b2.current, &["user_a"]);
6227 assert!(contacts_b2.incoming_requests.is_empty());
6228
6229 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
6230 let contacts_c = client_c.summarize_contacts(cx_c);
6231 assert!(contacts_c.current.is_empty());
6232 assert!(contacts_c.outgoing_requests.is_empty());
6233 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
6234 assert!(contacts_c2.current.is_empty());
6235 assert!(contacts_c2.outgoing_requests.is_empty());
6236
6237 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
6238 disconnect_and_reconnect(&client_a, cx_a).await;
6239 disconnect_and_reconnect(&client_b, cx_b).await;
6240 disconnect_and_reconnect(&client_c, cx_c).await;
6241 executor.run_until_parked();
6242 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6243 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6244 assert!(
6245 client_b
6246 .summarize_contacts(cx_b)
6247 .incoming_requests
6248 .is_empty()
6249 );
6250 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6251 assert!(
6252 client_c
6253 .summarize_contacts(cx_c)
6254 .outgoing_requests
6255 .is_empty()
6256 );
6257
6258 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
6259 client.disconnect(&cx.to_async());
6260 client.clear_contacts(cx).await;
6261 client
6262 .authenticate_and_connect(false, &cx.to_async())
6263 .await
6264 .into_response()
6265 .unwrap();
6266 }
6267}
6268
6269#[gpui::test(iterations = 10)]
6270async fn test_join_call_after_screen_was_shared(
6271 executor: BackgroundExecutor,
6272 cx_a: &mut TestAppContext,
6273 cx_b: &mut TestAppContext,
6274) {
6275 let mut server = TestServer::start(executor.clone()).await;
6276
6277 let client_a = server.create_client(cx_a, "user_a").await;
6278 let client_b = server.create_client(cx_b, "user_b").await;
6279 server
6280 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6281 .await;
6282
6283 let active_call_a = cx_a.read(ActiveCall::global);
6284 let active_call_b = cx_b.read(ActiveCall::global);
6285
6286 // Call users B and C from client A.
6287 active_call_a
6288 .update(cx_a, |call, cx| {
6289 call.invite(client_b.user_id().unwrap(), None, cx)
6290 })
6291 .await
6292 .unwrap();
6293
6294 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
6295 executor.run_until_parked();
6296 assert_eq!(
6297 room_participants(&room_a, cx_a),
6298 RoomParticipants {
6299 remote: Default::default(),
6300 pending: vec!["user_b".to_string()]
6301 }
6302 );
6303
6304 // User B receives the call.
6305
6306 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
6307 let call_b = incoming_call_b.next().await.unwrap().unwrap();
6308 assert_eq!(call_b.calling_user.github_login, "user_a");
6309
6310 // User A shares their screen
6311 let display = gpui::TestScreenCaptureSource::new();
6312 cx_a.set_screen_capture_sources(vec![display]);
6313 active_call_a
6314 .update(cx_a, |call, cx| {
6315 call.room()
6316 .unwrap()
6317 .update(cx, |room, cx| room.share_screen(cx))
6318 })
6319 .await
6320 .unwrap();
6321
6322 client_b.user_store().update(cx_b, |user_store, _| {
6323 user_store.clear_cache();
6324 });
6325
6326 // User B joins the room
6327 active_call_b
6328 .update(cx_b, |call, cx| call.accept_incoming(cx))
6329 .await
6330 .unwrap();
6331
6332 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
6333 assert!(incoming_call_b.next().await.unwrap().is_none());
6334
6335 executor.run_until_parked();
6336 assert_eq!(
6337 room_participants(&room_a, cx_a),
6338 RoomParticipants {
6339 remote: vec!["user_b".to_string()],
6340 pending: vec![],
6341 }
6342 );
6343 assert_eq!(
6344 room_participants(&room_b, cx_b),
6345 RoomParticipants {
6346 remote: vec!["user_a".to_string()],
6347 pending: vec![],
6348 }
6349 );
6350
6351 // Ensure User B sees User A's screenshare.
6352
6353 room_b.read_with(cx_b, |room, _| {
6354 assert_eq!(
6355 room.remote_participants()
6356 .get(&client_a.user_id().unwrap())
6357 .unwrap()
6358 .video_tracks
6359 .len(),
6360 1
6361 );
6362 });
6363}
6364
6365#[gpui::test]
6366async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
6367 let mut server = TestServer::start(cx.executor().clone()).await;
6368 let client_a = server.create_client(cx, "user_a").await;
6369 let (_workspace_a, cx) = client_a.build_test_workspace(cx).await;
6370
6371 cx.simulate_resize(size(px(300.), px(300.)));
6372
6373 cx.simulate_keystrokes("cmd-n cmd-n cmd-n");
6374 cx.update(|window, _cx| window.refresh());
6375
6376 let tab_bounds = cx.debug_bounds("TAB-2").unwrap();
6377 let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap();
6378
6379 assert!(
6380 tab_bounds.intersects(&new_tab_button_bounds),
6381 "Tab should overlap with the new tab button, if this is failing check if there's been a redesign!"
6382 );
6383
6384 cx.simulate_event(MouseDownEvent {
6385 button: MouseButton::Right,
6386 position: new_tab_button_bounds.center(),
6387 modifiers: Modifiers::default(),
6388 click_count: 1,
6389 first_mouse: false,
6390 });
6391
6392 // regression test that the right click menu for tabs does not open.
6393 assert!(cx.debug_bounds("MENU_ITEM-Close").is_none());
6394
6395 let tab_bounds = cx.debug_bounds("TAB-1").unwrap();
6396 cx.simulate_event(MouseDownEvent {
6397 button: MouseButton::Right,
6398 position: tab_bounds.center(),
6399 modifiers: Modifiers::default(),
6400 click_count: 1,
6401 first_mouse: false,
6402 });
6403 assert!(cx.debug_bounds("MENU_ITEM-Close").is_some());
6404}
6405
6406#[gpui::test]
6407async fn test_pane_split_left(cx: &mut TestAppContext) {
6408 let (_, client) = TestServer::start1(cx).await;
6409 let (workspace, cx) = client.build_test_workspace(cx).await;
6410
6411 cx.simulate_keystrokes("cmd-n");
6412 workspace.update(cx, |workspace, cx| {
6413 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 1);
6414 });
6415 cx.simulate_keystrokes("cmd-k left");
6416 workspace.update(cx, |workspace, cx| {
6417 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
6418 });
6419 cx.simulate_keystrokes("cmd-k");
6420 // sleep for longer than the timeout in keyboard shortcut handling
6421 // to verify that it doesn't fire in this case.
6422 cx.executor().advance_clock(Duration::from_secs(2));
6423 cx.simulate_keystrokes("left");
6424 workspace.update(cx, |workspace, cx| {
6425 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
6426 });
6427}
6428
6429#[gpui::test]
6430async fn test_join_after_restart(cx1: &mut TestAppContext, cx2: &mut TestAppContext) {
6431 let (mut server, client) = TestServer::start1(cx1).await;
6432 let channel1 = server.make_public_channel("channel1", &client, cx1).await;
6433 let channel2 = server.make_public_channel("channel2", &client, cx1).await;
6434
6435 join_channel(channel1, &client, cx1).await.unwrap();
6436 drop(client);
6437
6438 let client2 = server.create_client(cx2, "user_a").await;
6439 join_channel(channel2, &client2, cx2).await.unwrap();
6440}
6441
6442#[gpui::test]
6443async fn test_preview_tabs(cx: &mut TestAppContext) {
6444 let (_server, client) = TestServer::start1(cx).await;
6445 let (workspace, cx) = client.build_test_workspace(cx).await;
6446 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone());
6447
6448 let worktree_id = project.update(cx, |project, cx| {
6449 project.worktrees(cx).next().unwrap().read(cx).id()
6450 });
6451
6452 let path_1 = ProjectPath {
6453 worktree_id,
6454 path: Path::new("1.txt").into(),
6455 };
6456 let path_2 = ProjectPath {
6457 worktree_id,
6458 path: Path::new("2.js").into(),
6459 };
6460 let path_3 = ProjectPath {
6461 worktree_id,
6462 path: Path::new("3.rs").into(),
6463 };
6464
6465 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6466
6467 let get_path = |pane: &Pane, idx: usize, cx: &App| {
6468 pane.item_for_index(idx).unwrap().project_path(cx).unwrap()
6469 };
6470
6471 // Opening item 3 as a "permanent" tab
6472 workspace
6473 .update_in(cx, |workspace, window, cx| {
6474 workspace.open_path(path_3.clone(), None, false, window, cx)
6475 })
6476 .await
6477 .unwrap();
6478
6479 pane.update(cx, |pane, cx| {
6480 assert_eq!(pane.items_len(), 1);
6481 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6482 assert_eq!(pane.preview_item_id(), None);
6483
6484 assert!(!pane.can_navigate_backward());
6485 assert!(!pane.can_navigate_forward());
6486 });
6487
6488 // Open item 1 as preview
6489 workspace
6490 .update_in(cx, |workspace, window, cx| {
6491 workspace.open_path_preview(path_1.clone(), None, true, true, true, window, cx)
6492 })
6493 .await
6494 .unwrap();
6495
6496 pane.update(cx, |pane, cx| {
6497 assert_eq!(pane.items_len(), 2);
6498 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6499 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6500 assert_eq!(
6501 pane.preview_item_id(),
6502 Some(pane.items().nth(1).unwrap().item_id())
6503 );
6504
6505 assert!(pane.can_navigate_backward());
6506 assert!(!pane.can_navigate_forward());
6507 });
6508
6509 // Open item 2 as preview
6510 workspace
6511 .update_in(cx, |workspace, window, cx| {
6512 workspace.open_path_preview(path_2.clone(), None, true, true, true, window, cx)
6513 })
6514 .await
6515 .unwrap();
6516
6517 pane.update(cx, |pane, cx| {
6518 assert_eq!(pane.items_len(), 2);
6519 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6520 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6521 assert_eq!(
6522 pane.preview_item_id(),
6523 Some(pane.items().nth(1).unwrap().item_id())
6524 );
6525
6526 assert!(pane.can_navigate_backward());
6527 assert!(!pane.can_navigate_forward());
6528 });
6529
6530 // Going back should show item 1 as preview
6531 workspace
6532 .update_in(cx, |workspace, window, cx| {
6533 workspace.go_back(pane.downgrade(), window, cx)
6534 })
6535 .await
6536 .unwrap();
6537
6538 pane.update(cx, |pane, cx| {
6539 assert_eq!(pane.items_len(), 2);
6540 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6541 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6542 assert_eq!(
6543 pane.preview_item_id(),
6544 Some(pane.items().nth(1).unwrap().item_id())
6545 );
6546
6547 assert!(pane.can_navigate_backward());
6548 assert!(pane.can_navigate_forward());
6549 });
6550
6551 // Closing item 1
6552 pane.update_in(cx, |pane, window, cx| {
6553 pane.close_item_by_id(
6554 pane.active_item().unwrap().item_id(),
6555 workspace::SaveIntent::Skip,
6556 window,
6557 cx,
6558 )
6559 })
6560 .await
6561 .unwrap();
6562
6563 pane.update(cx, |pane, cx| {
6564 assert_eq!(pane.items_len(), 1);
6565 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6566 assert_eq!(pane.preview_item_id(), None);
6567
6568 assert!(pane.can_navigate_backward());
6569 assert!(!pane.can_navigate_forward());
6570 });
6571
6572 // Going back should show item 1 as preview
6573 workspace
6574 .update_in(cx, |workspace, window, cx| {
6575 workspace.go_back(pane.downgrade(), window, cx)
6576 })
6577 .await
6578 .unwrap();
6579
6580 pane.update(cx, |pane, cx| {
6581 assert_eq!(pane.items_len(), 2);
6582 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6583 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6584 assert_eq!(
6585 pane.preview_item_id(),
6586 Some(pane.items().nth(1).unwrap().item_id())
6587 );
6588
6589 assert!(pane.can_navigate_backward());
6590 assert!(pane.can_navigate_forward());
6591 });
6592
6593 // Close permanent tab
6594 pane.update_in(cx, |pane, window, cx| {
6595 let id = pane.items().next().unwrap().item_id();
6596 pane.close_item_by_id(id, workspace::SaveIntent::Skip, window, cx)
6597 })
6598 .await
6599 .unwrap();
6600
6601 pane.update(cx, |pane, cx| {
6602 assert_eq!(pane.items_len(), 1);
6603 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6604 assert_eq!(
6605 pane.preview_item_id(),
6606 Some(pane.items().next().unwrap().item_id())
6607 );
6608
6609 assert!(pane.can_navigate_backward());
6610 assert!(pane.can_navigate_forward());
6611 });
6612
6613 // Split pane to the right
6614 pane.update(cx, |pane, cx| {
6615 pane.split(workspace::SplitDirection::Right, cx);
6616 });
6617
6618 let right_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6619
6620 pane.update(cx, |pane, cx| {
6621 assert_eq!(pane.items_len(), 1);
6622 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6623 assert_eq!(
6624 pane.preview_item_id(),
6625 Some(pane.items().next().unwrap().item_id())
6626 );
6627
6628 assert!(pane.can_navigate_backward());
6629 assert!(pane.can_navigate_forward());
6630 });
6631
6632 right_pane.update(cx, |pane, cx| {
6633 assert_eq!(pane.items_len(), 1);
6634 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6635 assert_eq!(pane.preview_item_id(), None);
6636
6637 assert!(!pane.can_navigate_backward());
6638 assert!(!pane.can_navigate_forward());
6639 });
6640
6641 // Open item 2 as preview in right pane
6642 workspace
6643 .update_in(cx, |workspace, window, cx| {
6644 workspace.open_path_preview(path_2.clone(), None, true, true, true, window, cx)
6645 })
6646 .await
6647 .unwrap();
6648
6649 pane.update(cx, |pane, cx| {
6650 assert_eq!(pane.items_len(), 1);
6651 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6652 assert_eq!(
6653 pane.preview_item_id(),
6654 Some(pane.items().next().unwrap().item_id())
6655 );
6656
6657 assert!(pane.can_navigate_backward());
6658 assert!(pane.can_navigate_forward());
6659 });
6660
6661 right_pane.update(cx, |pane, cx| {
6662 assert_eq!(pane.items_len(), 2);
6663 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6664 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6665 assert_eq!(
6666 pane.preview_item_id(),
6667 Some(pane.items().nth(1).unwrap().item_id())
6668 );
6669
6670 assert!(pane.can_navigate_backward());
6671 assert!(!pane.can_navigate_forward());
6672 });
6673
6674 // Focus left pane
6675 workspace.update_in(cx, |workspace, window, cx| {
6676 workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx)
6677 });
6678
6679 // Open item 2 as preview in left pane
6680 workspace
6681 .update_in(cx, |workspace, window, cx| {
6682 workspace.open_path_preview(path_2.clone(), None, true, true, true, window, cx)
6683 })
6684 .await
6685 .unwrap();
6686
6687 pane.update(cx, |pane, cx| {
6688 assert_eq!(pane.items_len(), 1);
6689 assert_eq!(get_path(pane, 0, cx), path_2.clone());
6690 assert_eq!(
6691 pane.preview_item_id(),
6692 Some(pane.items().next().unwrap().item_id())
6693 );
6694
6695 assert!(pane.can_navigate_backward());
6696 assert!(!pane.can_navigate_forward());
6697 });
6698
6699 right_pane.update(cx, |pane, cx| {
6700 assert_eq!(pane.items_len(), 2);
6701 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6702 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6703 assert_eq!(
6704 pane.preview_item_id(),
6705 Some(pane.items().nth(1).unwrap().item_id())
6706 );
6707
6708 assert!(pane.can_navigate_backward());
6709 assert!(!pane.can_navigate_forward());
6710 });
6711}
6712
6713#[gpui::test(iterations = 10)]
6714async fn test_context_collaboration_with_reconnect(
6715 executor: BackgroundExecutor,
6716 cx_a: &mut TestAppContext,
6717 cx_b: &mut TestAppContext,
6718) {
6719 let mut server = TestServer::start(executor.clone()).await;
6720 let client_a = server.create_client(cx_a, "user_a").await;
6721 let client_b = server.create_client(cx_b, "user_b").await;
6722 server
6723 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6724 .await;
6725 let active_call_a = cx_a.read(ActiveCall::global);
6726
6727 client_a.fs().insert_tree("/a", Default::default()).await;
6728 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
6729 let project_id = active_call_a
6730 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6731 .await
6732 .unwrap();
6733 let project_b = client_b.join_remote_project(project_id, cx_b).await;
6734
6735 // Client A sees that a guest has joined.
6736 executor.run_until_parked();
6737
6738 project_a.read_with(cx_a, |project, _| {
6739 assert_eq!(project.collaborators().len(), 1);
6740 });
6741 project_b.read_with(cx_b, |project, _| {
6742 assert_eq!(project.collaborators().len(), 1);
6743 });
6744
6745 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
6746 let context_store_a = cx_a
6747 .update(|cx| {
6748 ContextStore::new(
6749 project_a.clone(),
6750 prompt_builder.clone(),
6751 Arc::new(SlashCommandWorkingSet::default()),
6752 cx,
6753 )
6754 })
6755 .await
6756 .unwrap();
6757 let context_store_b = cx_b
6758 .update(|cx| {
6759 ContextStore::new(
6760 project_b.clone(),
6761 prompt_builder.clone(),
6762 Arc::new(SlashCommandWorkingSet::default()),
6763 cx,
6764 )
6765 })
6766 .await
6767 .unwrap();
6768
6769 // Client A creates a new chats.
6770 let context_a = context_store_a.update(cx_a, |store, cx| store.create(cx));
6771 executor.run_until_parked();
6772
6773 // Client B retrieves host's contexts and joins one.
6774 let context_b = context_store_b
6775 .update(cx_b, |store, cx| {
6776 let host_contexts = store.host_contexts().to_vec();
6777 assert_eq!(host_contexts.len(), 1);
6778 store.open_remote_context(host_contexts[0].id.clone(), cx)
6779 })
6780 .await
6781 .unwrap();
6782
6783 // Host and guest make changes
6784 context_a.update(cx_a, |context, cx| {
6785 context.buffer().update(cx, |buffer, cx| {
6786 buffer.edit([(0..0, "Host change\n")], None, cx)
6787 })
6788 });
6789 context_b.update(cx_b, |context, cx| {
6790 context.buffer().update(cx, |buffer, cx| {
6791 buffer.edit([(0..0, "Guest change\n")], None, cx)
6792 })
6793 });
6794 executor.run_until_parked();
6795 assert_eq!(
6796 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6797 "Guest change\nHost change\n"
6798 );
6799 assert_eq!(
6800 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6801 "Guest change\nHost change\n"
6802 );
6803
6804 // Disconnect client A and make some changes while disconnected.
6805 server.disconnect_client(client_a.peer_id().unwrap());
6806 server.forbid_connections();
6807 context_a.update(cx_a, |context, cx| {
6808 context.buffer().update(cx, |buffer, cx| {
6809 buffer.edit([(0..0, "Host offline change\n")], None, cx)
6810 })
6811 });
6812 context_b.update(cx_b, |context, cx| {
6813 context.buffer().update(cx, |buffer, cx| {
6814 buffer.edit([(0..0, "Guest offline change\n")], None, cx)
6815 })
6816 });
6817 executor.run_until_parked();
6818 assert_eq!(
6819 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6820 "Host offline change\nGuest change\nHost change\n"
6821 );
6822 assert_eq!(
6823 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6824 "Guest offline change\nGuest change\nHost change\n"
6825 );
6826
6827 // Allow client A to reconnect and verify that contexts converge.
6828 server.allow_connections();
6829 executor.advance_clock(RECEIVE_TIMEOUT);
6830 assert_eq!(
6831 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6832 "Guest offline change\nHost offline change\nGuest change\nHost change\n"
6833 );
6834 assert_eq!(
6835 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6836 "Guest offline change\nHost offline change\nGuest change\nHost change\n"
6837 );
6838
6839 // Client A disconnects without being able to reconnect. Context B becomes readonly.
6840 server.forbid_connections();
6841 server.disconnect_client(client_a.peer_id().unwrap());
6842 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
6843 context_b.read_with(cx_b, |context, cx| {
6844 assert!(context.buffer().read(cx).read_only());
6845 });
6846}
6847
6848#[gpui::test]
6849async fn test_remote_git_branches(
6850 executor: BackgroundExecutor,
6851 cx_a: &mut TestAppContext,
6852 cx_b: &mut TestAppContext,
6853) {
6854 let mut server = TestServer::start(executor.clone()).await;
6855 let client_a = server.create_client(cx_a, "user_a").await;
6856 let client_b = server.create_client(cx_b, "user_b").await;
6857 server
6858 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6859 .await;
6860 let active_call_a = cx_a.read(ActiveCall::global);
6861
6862 client_a
6863 .fs()
6864 .insert_tree("/project", serde_json::json!({ ".git":{} }))
6865 .await;
6866 let branches = ["main", "dev", "feature-1"];
6867 client_a
6868 .fs()
6869 .insert_branches(Path::new("/project/.git"), &branches);
6870 let branches_set = branches
6871 .into_iter()
6872 .map(ToString::to_string)
6873 .collect::<HashSet<_>>();
6874
6875 let (project_a, _) = client_a.build_local_project("/project", cx_a).await;
6876
6877 let project_id = active_call_a
6878 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6879 .await
6880 .unwrap();
6881 let project_b = client_b.join_remote_project(project_id, cx_b).await;
6882
6883 // Client A sees that a guest has joined and the repo has been populated
6884 executor.run_until_parked();
6885
6886 let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
6887
6888 let branches_b = cx_b
6889 .update(|cx| repo_b.update(cx, |repository, _| repository.branches()))
6890 .await
6891 .unwrap()
6892 .unwrap();
6893
6894 let new_branch = branches[2];
6895
6896 let branches_b = branches_b
6897 .into_iter()
6898 .map(|branch| branch.name().to_string())
6899 .collect::<HashSet<_>>();
6900
6901 assert_eq!(branches_b, branches_set);
6902
6903 cx_b.update(|cx| {
6904 repo_b.update(cx, |repository, _cx| {
6905 repository.change_branch(new_branch.to_string())
6906 })
6907 })
6908 .await
6909 .unwrap()
6910 .unwrap();
6911
6912 executor.run_until_parked();
6913
6914 let host_branch = cx_a.update(|cx| {
6915 project_a.update(cx, |project, cx| {
6916 project
6917 .repositories(cx)
6918 .values()
6919 .next()
6920 .unwrap()
6921 .read(cx)
6922 .branch
6923 .as_ref()
6924 .unwrap()
6925 .clone()
6926 })
6927 });
6928
6929 assert_eq!(host_branch.name(), branches[2]);
6930
6931 // Also try creating a new branch
6932 cx_b.update(|cx| {
6933 repo_b.update(cx, |repository, _cx| {
6934 repository.create_branch("totally-new-branch".to_string())
6935 })
6936 })
6937 .await
6938 .unwrap()
6939 .unwrap();
6940
6941 cx_b.update(|cx| {
6942 repo_b.update(cx, |repository, _cx| {
6943 repository.change_branch("totally-new-branch".to_string())
6944 })
6945 })
6946 .await
6947 .unwrap()
6948 .unwrap();
6949
6950 executor.run_until_parked();
6951
6952 let host_branch = cx_a.update(|cx| {
6953 project_a.update(cx, |project, cx| {
6954 project
6955 .repositories(cx)
6956 .values()
6957 .next()
6958 .unwrap()
6959 .read(cx)
6960 .branch
6961 .as_ref()
6962 .unwrap()
6963 .clone()
6964 })
6965 });
6966
6967 assert_eq!(host_branch.name(), "totally-new-branch");
6968}