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