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