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