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