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