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