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