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 .collect::<Vec<_>>(),
3509 &[
3510 (Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
3511 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3512 ]
3513 )
3514 });
3515
3516 // As client A, update a settings file. As Client B, see the changed settings.
3517 client_a
3518 .fs()
3519 .insert_file("/dir/.zed/settings.json", r#"{}"#.into())
3520 .await;
3521 executor.run_until_parked();
3522 cx_b.read(|cx| {
3523 let store = cx.global::<SettingsStore>();
3524 assert_eq!(
3525 store
3526 .local_settings(worktree_b.read(cx).id())
3527 .collect::<Vec<_>>(),
3528 &[
3529 (Path::new("").into(), r#"{}"#.to_string()),
3530 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3531 ]
3532 )
3533 });
3534
3535 // As client A, create and remove some settings files. As client B, see the changed settings.
3536 client_a
3537 .fs()
3538 .remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
3539 .await
3540 .unwrap();
3541 client_a
3542 .fs()
3543 .create_dir("/dir/b/.zed".as_ref())
3544 .await
3545 .unwrap();
3546 client_a
3547 .fs()
3548 .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
3549 .await;
3550 executor.run_until_parked();
3551 cx_b.read(|cx| {
3552 let store = cx.global::<SettingsStore>();
3553 assert_eq!(
3554 store
3555 .local_settings(worktree_b.read(cx).id())
3556 .collect::<Vec<_>>(),
3557 &[
3558 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3559 (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
3560 ]
3561 )
3562 });
3563
3564 // As client B, disconnect.
3565 server.forbid_connections();
3566 server.disconnect_client(client_b.peer_id().unwrap());
3567
3568 // As client A, change and remove settings files while client B is disconnected.
3569 client_a
3570 .fs()
3571 .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
3572 .await;
3573 client_a
3574 .fs()
3575 .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
3576 .await
3577 .unwrap();
3578 executor.run_until_parked();
3579
3580 // As client B, reconnect and see the changed settings.
3581 server.allow_connections();
3582 executor.advance_clock(RECEIVE_TIMEOUT);
3583 cx_b.read(|cx| {
3584 let store = cx.global::<SettingsStore>();
3585 assert_eq!(
3586 store
3587 .local_settings(worktree_b.read(cx).id())
3588 .collect::<Vec<_>>(),
3589 &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
3590 )
3591 });
3592}
3593
3594#[gpui::test(iterations = 10)]
3595async fn test_buffer_conflict_after_save(
3596 executor: BackgroundExecutor,
3597 cx_a: &mut TestAppContext,
3598 cx_b: &mut TestAppContext,
3599) {
3600 let mut server = TestServer::start(executor.clone()).await;
3601 let client_a = server.create_client(cx_a, "user_a").await;
3602 let client_b = server.create_client(cx_b, "user_b").await;
3603 server
3604 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3605 .await;
3606 let active_call_a = cx_a.read(ActiveCall::global);
3607
3608 client_a
3609 .fs()
3610 .insert_tree(
3611 path!("/dir"),
3612 json!({
3613 "a.txt": "a-contents",
3614 }),
3615 )
3616 .await;
3617 let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
3618 let project_id = active_call_a
3619 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3620 .await
3621 .unwrap();
3622 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3623
3624 // Open a buffer as client B
3625 let buffer_b = project_b
3626 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3627 .await
3628 .unwrap();
3629
3630 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
3631
3632 buffer_b.read_with(cx_b, |buf, _| {
3633 assert!(buf.is_dirty());
3634 assert!(!buf.has_conflict());
3635 });
3636
3637 project_b
3638 .update(cx_b, |project, cx| {
3639 project.save_buffer(buffer_b.clone(), cx)
3640 })
3641 .await
3642 .unwrap();
3643
3644 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
3645
3646 buffer_b.read_with(cx_b, |buf, _| {
3647 assert!(!buf.has_conflict());
3648 });
3649
3650 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
3651
3652 buffer_b.read_with(cx_b, |buf, _| {
3653 assert!(buf.is_dirty());
3654 assert!(!buf.has_conflict());
3655 });
3656}
3657
3658#[gpui::test(iterations = 10)]
3659async fn test_buffer_reloading(
3660 executor: BackgroundExecutor,
3661 cx_a: &mut TestAppContext,
3662 cx_b: &mut TestAppContext,
3663) {
3664 let mut server = TestServer::start(executor.clone()).await;
3665 let client_a = server.create_client(cx_a, "user_a").await;
3666 let client_b = server.create_client(cx_b, "user_b").await;
3667 server
3668 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3669 .await;
3670 let active_call_a = cx_a.read(ActiveCall::global);
3671
3672 client_a
3673 .fs()
3674 .insert_tree(
3675 path!("/dir"),
3676 json!({
3677 "a.txt": "a\nb\nc",
3678 }),
3679 )
3680 .await;
3681 let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
3682 let project_id = active_call_a
3683 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3684 .await
3685 .unwrap();
3686 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3687
3688 // Open a buffer as client B
3689 let buffer_b = project_b
3690 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3691 .await
3692 .unwrap();
3693
3694 buffer_b.read_with(cx_b, |buf, _| {
3695 assert!(!buf.is_dirty());
3696 assert!(!buf.has_conflict());
3697 assert_eq!(buf.line_ending(), LineEnding::Unix);
3698 });
3699
3700 let new_contents = Rope::from("d\ne\nf");
3701 client_a
3702 .fs()
3703 .save(
3704 path!("/dir/a.txt").as_ref(),
3705 &new_contents,
3706 LineEnding::Windows,
3707 )
3708 .await
3709 .unwrap();
3710
3711 executor.run_until_parked();
3712
3713 buffer_b.read_with(cx_b, |buf, _| {
3714 assert_eq!(buf.text(), new_contents.to_string());
3715 assert!(!buf.is_dirty());
3716 assert!(!buf.has_conflict());
3717 assert_eq!(buf.line_ending(), LineEnding::Windows);
3718 });
3719}
3720
3721#[gpui::test(iterations = 10)]
3722async fn test_editing_while_guest_opens_buffer(
3723 executor: BackgroundExecutor,
3724 cx_a: &mut TestAppContext,
3725 cx_b: &mut TestAppContext,
3726) {
3727 let mut server = TestServer::start(executor.clone()).await;
3728 let client_a = server.create_client(cx_a, "user_a").await;
3729 let client_b = server.create_client(cx_b, "user_b").await;
3730 server
3731 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3732 .await;
3733 let active_call_a = cx_a.read(ActiveCall::global);
3734
3735 client_a
3736 .fs()
3737 .insert_tree(path!("/dir"), json!({ "a.txt": "a-contents" }))
3738 .await;
3739 let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
3740 let project_id = active_call_a
3741 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3742 .await
3743 .unwrap();
3744 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3745
3746 // Open a buffer as client A
3747 let buffer_a = project_a
3748 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3749 .await
3750 .unwrap();
3751
3752 // Start opening the same buffer as client B
3753 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3754 let buffer_b = cx_b.executor().spawn(open_buffer);
3755
3756 // Edit the buffer as client A while client B is still opening it.
3757 cx_b.executor().simulate_random_delay().await;
3758 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
3759 cx_b.executor().simulate_random_delay().await;
3760 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
3761
3762 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3763 let buffer_b = buffer_b.await.unwrap();
3764 executor.run_until_parked();
3765
3766 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3767}
3768
3769#[gpui::test(iterations = 10)]
3770async fn test_leaving_worktree_while_opening_buffer(
3771 executor: BackgroundExecutor,
3772 cx_a: &mut TestAppContext,
3773 cx_b: &mut TestAppContext,
3774) {
3775 let mut server = TestServer::start(executor.clone()).await;
3776 let client_a = server.create_client(cx_a, "user_a").await;
3777 let client_b = server.create_client(cx_b, "user_b").await;
3778 server
3779 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3780 .await;
3781 let active_call_a = cx_a.read(ActiveCall::global);
3782
3783 client_a
3784 .fs()
3785 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3786 .await;
3787 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3788 let project_id = active_call_a
3789 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3790 .await
3791 .unwrap();
3792 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3793
3794 // See that a guest has joined as client A.
3795 executor.run_until_parked();
3796
3797 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3798
3799 // Begin opening a buffer as client B, but leave the project before the open completes.
3800 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3801 let buffer_b = cx_b.executor().spawn(open_buffer);
3802 cx_b.update(|_| drop(project_b));
3803 drop(buffer_b);
3804
3805 // See that the guest has left.
3806 executor.run_until_parked();
3807
3808 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3809}
3810
3811#[gpui::test(iterations = 10)]
3812async fn test_canceling_buffer_opening(
3813 executor: BackgroundExecutor,
3814 cx_a: &mut TestAppContext,
3815 cx_b: &mut TestAppContext,
3816) {
3817 let mut server = TestServer::start(executor.clone()).await;
3818 let client_a = server.create_client(cx_a, "user_a").await;
3819 let client_b = server.create_client(cx_b, "user_b").await;
3820 server
3821 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3822 .await;
3823 let active_call_a = cx_a.read(ActiveCall::global);
3824
3825 client_a
3826 .fs()
3827 .insert_tree(
3828 "/dir",
3829 json!({
3830 "a.txt": "abc",
3831 }),
3832 )
3833 .await;
3834 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3835 let project_id = active_call_a
3836 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3837 .await
3838 .unwrap();
3839 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3840
3841 let buffer_a = project_a
3842 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3843 .await
3844 .unwrap();
3845
3846 // Open a buffer as client B but cancel after a random amount of time.
3847 let buffer_b = project_b.update(cx_b, |p, cx| {
3848 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3849 });
3850 executor.simulate_random_delay().await;
3851 drop(buffer_b);
3852
3853 // Try opening the same buffer again as client B, and ensure we can
3854 // still do it despite the cancellation above.
3855 let buffer_b = project_b
3856 .update(cx_b, |p, cx| {
3857 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3858 })
3859 .await
3860 .unwrap();
3861
3862 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3863}
3864
3865#[gpui::test(iterations = 10)]
3866async fn test_leaving_project(
3867 executor: BackgroundExecutor,
3868 cx_a: &mut TestAppContext,
3869 cx_b: &mut TestAppContext,
3870 cx_c: &mut TestAppContext,
3871) {
3872 let mut server = TestServer::start(executor.clone()).await;
3873 let client_a = server.create_client(cx_a, "user_a").await;
3874 let client_b = server.create_client(cx_b, "user_b").await;
3875 let client_c = server.create_client(cx_c, "user_c").await;
3876 server
3877 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3878 .await;
3879 let active_call_a = cx_a.read(ActiveCall::global);
3880
3881 client_a
3882 .fs()
3883 .insert_tree(
3884 "/a",
3885 json!({
3886 "a.txt": "a-contents",
3887 "b.txt": "b-contents",
3888 }),
3889 )
3890 .await;
3891 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3892 let project_id = active_call_a
3893 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3894 .await
3895 .unwrap();
3896 let project_b1 = client_b.join_remote_project(project_id, cx_b).await;
3897 let project_c = client_c.join_remote_project(project_id, cx_c).await;
3898
3899 // Client A sees that a guest has joined.
3900 executor.run_until_parked();
3901
3902 project_a.read_with(cx_a, |project, _| {
3903 assert_eq!(project.collaborators().len(), 2);
3904 });
3905
3906 project_b1.read_with(cx_b, |project, _| {
3907 assert_eq!(project.collaborators().len(), 2);
3908 });
3909
3910 project_c.read_with(cx_c, |project, _| {
3911 assert_eq!(project.collaborators().len(), 2);
3912 });
3913
3914 // Client B opens a buffer.
3915 let buffer_b1 = project_b1
3916 .update(cx_b, |project, cx| {
3917 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3918 project.open_buffer((worktree_id, "a.txt"), cx)
3919 })
3920 .await
3921 .unwrap();
3922
3923 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3924
3925 // Drop client B's project and ensure client A and client C observe client B leaving.
3926 cx_b.update(|_| drop(project_b1));
3927 executor.run_until_parked();
3928
3929 project_a.read_with(cx_a, |project, _| {
3930 assert_eq!(project.collaborators().len(), 1);
3931 });
3932
3933 project_c.read_with(cx_c, |project, _| {
3934 assert_eq!(project.collaborators().len(), 1);
3935 });
3936
3937 // Client B re-joins the project and can open buffers as before.
3938 let project_b2 = client_b.join_remote_project(project_id, cx_b).await;
3939 executor.run_until_parked();
3940
3941 project_a.read_with(cx_a, |project, _| {
3942 assert_eq!(project.collaborators().len(), 2);
3943 });
3944
3945 project_b2.read_with(cx_b, |project, _| {
3946 assert_eq!(project.collaborators().len(), 2);
3947 });
3948
3949 project_c.read_with(cx_c, |project, _| {
3950 assert_eq!(project.collaborators().len(), 2);
3951 });
3952
3953 let buffer_b2 = project_b2
3954 .update(cx_b, |project, cx| {
3955 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3956 project.open_buffer((worktree_id, "a.txt"), cx)
3957 })
3958 .await
3959 .unwrap();
3960
3961 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3962
3963 project_a.read_with(cx_a, |project, _| {
3964 assert_eq!(project.collaborators().len(), 2);
3965 });
3966
3967 // Drop client B's connection and ensure client A and client C observe client B leaving.
3968 client_b.disconnect(&cx_b.to_async());
3969 executor.advance_clock(RECONNECT_TIMEOUT);
3970
3971 project_a.read_with(cx_a, |project, _| {
3972 assert_eq!(project.collaborators().len(), 1);
3973 });
3974
3975 project_b2.read_with(cx_b, |project, cx| {
3976 assert!(project.is_disconnected(cx));
3977 });
3978
3979 project_c.read_with(cx_c, |project, _| {
3980 assert_eq!(project.collaborators().len(), 1);
3981 });
3982
3983 // Client B can't join the project, unless they re-join the room.
3984 cx_b.spawn(|cx| {
3985 Project::in_room(
3986 project_id,
3987 client_b.app_state.client.clone(),
3988 client_b.user_store().clone(),
3989 client_b.language_registry().clone(),
3990 FakeFs::new(cx.background_executor().clone()),
3991 cx,
3992 )
3993 })
3994 .await
3995 .unwrap_err();
3996
3997 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3998 client_c.wait_for_current_user(cx_c).await;
3999 server.forbid_connections();
4000 server.disconnect_client(client_c.peer_id().unwrap());
4001 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
4002 executor.run_until_parked();
4003
4004 project_a.read_with(cx_a, |project, _| {
4005 assert_eq!(project.collaborators().len(), 0);
4006 });
4007
4008 project_b2.read_with(cx_b, |project, cx| {
4009 assert!(project.is_disconnected(cx));
4010 });
4011
4012 project_c.read_with(cx_c, |project, cx| {
4013 assert!(project.is_disconnected(cx));
4014 });
4015}
4016
4017#[gpui::test(iterations = 10)]
4018async fn test_collaborating_with_diagnostics(
4019 executor: BackgroundExecutor,
4020 cx_a: &mut TestAppContext,
4021 cx_b: &mut TestAppContext,
4022 cx_c: &mut TestAppContext,
4023) {
4024 let mut server = TestServer::start(executor.clone()).await;
4025 let client_a = server.create_client(cx_a, "user_a").await;
4026 let client_b = server.create_client(cx_b, "user_b").await;
4027 let client_c = server.create_client(cx_c, "user_c").await;
4028 server
4029 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
4030 .await;
4031 let active_call_a = cx_a.read(ActiveCall::global);
4032
4033 client_a.language_registry().add(Arc::new(Language::new(
4034 LanguageConfig {
4035 name: "Rust".into(),
4036 matcher: LanguageMatcher {
4037 path_suffixes: vec!["rs".to_string()],
4038 ..Default::default()
4039 },
4040 ..Default::default()
4041 },
4042 Some(tree_sitter_rust::LANGUAGE.into()),
4043 )));
4044 let mut fake_language_servers = client_a
4045 .language_registry()
4046 .register_fake_lsp("Rust", Default::default());
4047
4048 // Share a project as client A
4049 client_a
4050 .fs()
4051 .insert_tree(
4052 path!("/a"),
4053 json!({
4054 "a.rs": "let one = two",
4055 "other.rs": "",
4056 }),
4057 )
4058 .await;
4059 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
4060
4061 // Cause the language server to start.
4062 let _buffer = project_a
4063 .update(cx_a, |project, cx| {
4064 project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
4065 })
4066 .await
4067 .unwrap();
4068
4069 // Simulate a language server reporting errors for a file.
4070 let mut fake_language_server = fake_language_servers.next().await.unwrap();
4071 fake_language_server
4072 .receive_notification::<lsp::notification::DidOpenTextDocument>()
4073 .await;
4074 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4075 &lsp::PublishDiagnosticsParams {
4076 uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
4077 version: None,
4078 diagnostics: vec![lsp::Diagnostic {
4079 severity: Some(lsp::DiagnosticSeverity::WARNING),
4080 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
4081 message: "message 0".to_string(),
4082 ..Default::default()
4083 }],
4084 },
4085 );
4086
4087 // Client A shares the project and, simultaneously, the language server
4088 // publishes a diagnostic. This is done to ensure that the server always
4089 // observes the latest diagnostics for a worktree.
4090 let project_id = active_call_a
4091 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4092 .await
4093 .unwrap();
4094 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4095 &lsp::PublishDiagnosticsParams {
4096 uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
4097 version: None,
4098 diagnostics: vec![lsp::Diagnostic {
4099 severity: Some(lsp::DiagnosticSeverity::ERROR),
4100 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
4101 message: "message 1".to_string(),
4102 ..Default::default()
4103 }],
4104 },
4105 );
4106
4107 // Join the worktree as client B.
4108 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4109
4110 // Wait for server to see the diagnostics update.
4111 executor.run_until_parked();
4112
4113 // Ensure client B observes the new diagnostics.
4114
4115 project_b.read_with(cx_b, |project, cx| {
4116 assert_eq!(
4117 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4118 &[(
4119 ProjectPath {
4120 worktree_id,
4121 path: Arc::from(Path::new("a.rs")),
4122 },
4123 LanguageServerId(0),
4124 DiagnosticSummary {
4125 error_count: 1,
4126 warning_count: 0,
4127 },
4128 )]
4129 )
4130 });
4131
4132 // Join project as client C and observe the diagnostics.
4133 let project_c = client_c.join_remote_project(project_id, cx_c).await;
4134 executor.run_until_parked();
4135 let project_c_diagnostic_summaries =
4136 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
4137 project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
4138 })));
4139 project_c.update(cx_c, |_, cx| {
4140 let summaries = project_c_diagnostic_summaries.clone();
4141 cx.subscribe(&project_c, {
4142 move |p, _, event, cx| {
4143 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
4144 *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
4145 }
4146 }
4147 })
4148 .detach();
4149 });
4150
4151 executor.run_until_parked();
4152 assert_eq!(
4153 project_c_diagnostic_summaries.borrow().as_slice(),
4154 &[(
4155 ProjectPath {
4156 worktree_id,
4157 path: Arc::from(Path::new("a.rs")),
4158 },
4159 LanguageServerId(0),
4160 DiagnosticSummary {
4161 error_count: 1,
4162 warning_count: 0,
4163 },
4164 )]
4165 );
4166
4167 // Simulate a language server reporting more errors for a file.
4168 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4169 &lsp::PublishDiagnosticsParams {
4170 uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
4171 version: None,
4172 diagnostics: vec![
4173 lsp::Diagnostic {
4174 severity: Some(lsp::DiagnosticSeverity::ERROR),
4175 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
4176 message: "message 1".to_string(),
4177 ..Default::default()
4178 },
4179 lsp::Diagnostic {
4180 severity: Some(lsp::DiagnosticSeverity::WARNING),
4181 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
4182 message: "message 2".to_string(),
4183 ..Default::default()
4184 },
4185 ],
4186 },
4187 );
4188
4189 // Clients B and C get the updated summaries
4190 executor.run_until_parked();
4191
4192 project_b.read_with(cx_b, |project, cx| {
4193 assert_eq!(
4194 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4195 [(
4196 ProjectPath {
4197 worktree_id,
4198 path: Arc::from(Path::new("a.rs")),
4199 },
4200 LanguageServerId(0),
4201 DiagnosticSummary {
4202 error_count: 1,
4203 warning_count: 1,
4204 },
4205 )]
4206 );
4207 });
4208
4209 project_c.read_with(cx_c, |project, cx| {
4210 assert_eq!(
4211 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4212 [(
4213 ProjectPath {
4214 worktree_id,
4215 path: Arc::from(Path::new("a.rs")),
4216 },
4217 LanguageServerId(0),
4218 DiagnosticSummary {
4219 error_count: 1,
4220 warning_count: 1,
4221 },
4222 )]
4223 );
4224 });
4225
4226 // Open the file with the errors on client B. They should be present.
4227 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4228 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4229
4230 buffer_b.read_with(cx_b, |buffer, _| {
4231 assert_eq!(
4232 buffer
4233 .snapshot()
4234 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
4235 .collect::<Vec<_>>(),
4236 &[
4237 DiagnosticEntry {
4238 range: Point::new(0, 4)..Point::new(0, 7),
4239 diagnostic: Diagnostic {
4240 group_id: 2,
4241 message: "message 1".to_string(),
4242 severity: lsp::DiagnosticSeverity::ERROR,
4243 is_primary: true,
4244 source_kind: DiagnosticSourceKind::Pushed,
4245 ..Diagnostic::default()
4246 }
4247 },
4248 DiagnosticEntry {
4249 range: Point::new(0, 10)..Point::new(0, 13),
4250 diagnostic: Diagnostic {
4251 group_id: 3,
4252 severity: lsp::DiagnosticSeverity::WARNING,
4253 message: "message 2".to_string(),
4254 is_primary: true,
4255 source_kind: DiagnosticSourceKind::Pushed,
4256 ..Diagnostic::default()
4257 }
4258 }
4259 ]
4260 );
4261 });
4262
4263 // Simulate a language server reporting no errors for a file.
4264 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4265 &lsp::PublishDiagnosticsParams {
4266 uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
4267 version: None,
4268 diagnostics: Vec::new(),
4269 },
4270 );
4271 executor.run_until_parked();
4272
4273 project_a.read_with(cx_a, |project, cx| {
4274 assert_eq!(
4275 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4276 []
4277 )
4278 });
4279
4280 project_b.read_with(cx_b, |project, cx| {
4281 assert_eq!(
4282 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4283 []
4284 )
4285 });
4286
4287 project_c.read_with(cx_c, |project, cx| {
4288 assert_eq!(
4289 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4290 []
4291 )
4292 });
4293}
4294
4295#[gpui::test(iterations = 10)]
4296async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
4297 executor: BackgroundExecutor,
4298 cx_a: &mut TestAppContext,
4299 cx_b: &mut TestAppContext,
4300) {
4301 let mut server = TestServer::start(executor.clone()).await;
4302 let client_a = server.create_client(cx_a, "user_a").await;
4303 let client_b = server.create_client(cx_b, "user_b").await;
4304 server
4305 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4306 .await;
4307
4308 client_a.language_registry().add(rust_lang());
4309 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4310 "Rust",
4311 FakeLspAdapter {
4312 disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
4313 disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
4314 ..Default::default()
4315 },
4316 );
4317
4318 let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
4319 client_a
4320 .fs()
4321 .insert_tree(
4322 path!("/test"),
4323 json!({
4324 "one.rs": "const ONE: usize = 1;",
4325 "two.rs": "const TWO: usize = 2;",
4326 "three.rs": "const THREE: usize = 3;",
4327 "four.rs": "const FOUR: usize = 3;",
4328 "five.rs": "const FIVE: usize = 3;",
4329 }),
4330 )
4331 .await;
4332
4333 let (project_a, worktree_id) = client_a.build_local_project(path!("/test"), cx_a).await;
4334
4335 // Share a project as client A
4336 let active_call_a = cx_a.read(ActiveCall::global);
4337 let project_id = active_call_a
4338 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4339 .await
4340 .unwrap();
4341
4342 // Join the project as client B and open all three files.
4343 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4344 let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
4345 project_b.update(cx_b, |p, cx| {
4346 p.open_buffer_with_lsp((worktree_id, file_name), cx)
4347 })
4348 }))
4349 .await
4350 .unwrap();
4351
4352 // Simulate a language server reporting errors for a file.
4353 let fake_language_server = fake_language_servers.next().await.unwrap();
4354 fake_language_server
4355 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
4356 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4357 })
4358 .await
4359 .into_response()
4360 .unwrap();
4361 fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
4362 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4363 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
4364 lsp::WorkDoneProgressBegin {
4365 title: "Progress Began".into(),
4366 ..Default::default()
4367 },
4368 )),
4369 });
4370 for file_name in file_names {
4371 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4372 &lsp::PublishDiagnosticsParams {
4373 uri: lsp::Uri::from_file_path(Path::new(path!("/test")).join(file_name)).unwrap(),
4374 version: None,
4375 diagnostics: vec![lsp::Diagnostic {
4376 severity: Some(lsp::DiagnosticSeverity::WARNING),
4377 source: Some("the-disk-based-diagnostics-source".into()),
4378 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
4379 message: "message one".to_string(),
4380 ..Default::default()
4381 }],
4382 },
4383 );
4384 }
4385 fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
4386 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4387 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
4388 lsp::WorkDoneProgressEnd { message: None },
4389 )),
4390 });
4391
4392 // When the "disk base diagnostics finished" message is received, the buffers'
4393 // diagnostics are expected to be present.
4394 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
4395 project_b.update(cx_b, {
4396 let project_b = project_b.clone();
4397 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
4398 move |_, cx| {
4399 cx.subscribe(&project_b, move |_, _, event, cx| {
4400 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
4401 disk_based_diagnostics_finished.store(true, SeqCst);
4402 for (buffer, _) in &guest_buffers {
4403 assert_eq!(
4404 buffer
4405 .read(cx)
4406 .snapshot()
4407 .diagnostics_in_range::<_, usize>(0..5, false)
4408 .count(),
4409 1,
4410 "expected a diagnostic for buffer {:?}",
4411 buffer.read(cx).file().unwrap().path(),
4412 );
4413 }
4414 }
4415 })
4416 .detach();
4417 }
4418 });
4419
4420 executor.run_until_parked();
4421 assert!(disk_based_diagnostics_finished.load(SeqCst));
4422}
4423
4424#[gpui::test(iterations = 10)]
4425async fn test_reloading_buffer_manually(
4426 executor: BackgroundExecutor,
4427 cx_a: &mut TestAppContext,
4428 cx_b: &mut TestAppContext,
4429) {
4430 let mut server = TestServer::start(executor.clone()).await;
4431 let client_a = server.create_client(cx_a, "user_a").await;
4432 let client_b = server.create_client(cx_b, "user_b").await;
4433 server
4434 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4435 .await;
4436 let active_call_a = cx_a.read(ActiveCall::global);
4437
4438 client_a
4439 .fs()
4440 .insert_tree(path!("/a"), json!({ "a.rs": "let one = 1;" }))
4441 .await;
4442 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
4443 let buffer_a = project_a
4444 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4445 .await
4446 .unwrap();
4447 let project_id = active_call_a
4448 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4449 .await
4450 .unwrap();
4451
4452 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4453
4454 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4455 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4456 buffer_b.update(cx_b, |buffer, cx| {
4457 buffer.edit([(4..7, "six")], None, cx);
4458 buffer.edit([(10..11, "6")], None, cx);
4459 assert_eq!(buffer.text(), "let six = 6;");
4460 assert!(buffer.is_dirty());
4461 assert!(!buffer.has_conflict());
4462 });
4463 executor.run_until_parked();
4464
4465 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4466
4467 client_a
4468 .fs()
4469 .save(
4470 path!("/a/a.rs").as_ref(),
4471 &Rope::from("let seven = 7;"),
4472 LineEnding::Unix,
4473 )
4474 .await
4475 .unwrap();
4476 executor.run_until_parked();
4477
4478 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4479
4480 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4481
4482 project_b
4483 .update(cx_b, |project, cx| {
4484 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4485 })
4486 .await
4487 .unwrap();
4488
4489 buffer_a.read_with(cx_a, |buffer, _| {
4490 assert_eq!(buffer.text(), "let seven = 7;");
4491 assert!(!buffer.is_dirty());
4492 assert!(!buffer.has_conflict());
4493 });
4494
4495 buffer_b.read_with(cx_b, |buffer, _| {
4496 assert_eq!(buffer.text(), "let seven = 7;");
4497 assert!(!buffer.is_dirty());
4498 assert!(!buffer.has_conflict());
4499 });
4500
4501 buffer_a.update(cx_a, |buffer, cx| {
4502 // Undoing on the host is a no-op when the reload was initiated by the guest.
4503 buffer.undo(cx);
4504 assert_eq!(buffer.text(), "let seven = 7;");
4505 assert!(!buffer.is_dirty());
4506 assert!(!buffer.has_conflict());
4507 });
4508 buffer_b.update(cx_b, |buffer, cx| {
4509 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4510 buffer.undo(cx);
4511 assert_eq!(buffer.text(), "let six = 6;");
4512 assert!(buffer.is_dirty());
4513 assert!(!buffer.has_conflict());
4514 });
4515}
4516
4517#[gpui::test(iterations = 10)]
4518async fn test_formatting_buffer(
4519 executor: BackgroundExecutor,
4520 cx_a: &mut TestAppContext,
4521 cx_b: &mut TestAppContext,
4522) {
4523 let mut server = TestServer::start(executor.clone()).await;
4524 let client_a = server.create_client(cx_a, "user_a").await;
4525 let client_b = server.create_client(cx_b, "user_b").await;
4526 server
4527 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4528 .await;
4529 let active_call_a = cx_a.read(ActiveCall::global);
4530
4531 client_a.language_registry().add(rust_lang());
4532 let mut fake_language_servers = client_a
4533 .language_registry()
4534 .register_fake_lsp("Rust", FakeLspAdapter::default());
4535
4536 // Here we insert a fake tree with a directory that exists on disk. This is needed
4537 // because later we'll invoke a command, which requires passing a working directory
4538 // that points to a valid location on disk.
4539 let directory = env::current_dir().unwrap();
4540 client_a
4541 .fs()
4542 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4543 .await;
4544 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4545 let project_id = active_call_a
4546 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4547 .await
4548 .unwrap();
4549 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4550
4551 let buffer_b = project_b
4552 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4553 .await
4554 .unwrap();
4555
4556 let _handle = project_b.update(cx_b, |project, cx| {
4557 project.register_buffer_with_language_servers(&buffer_b, cx)
4558 });
4559 let fake_language_server = fake_language_servers.next().await.unwrap();
4560 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(|_, _| async move {
4561 Ok(Some(vec![
4562 lsp::TextEdit {
4563 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4564 new_text: "h".to_string(),
4565 },
4566 lsp::TextEdit {
4567 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4568 new_text: "y".to_string(),
4569 },
4570 ]))
4571 });
4572
4573 project_b
4574 .update(cx_b, |project, cx| {
4575 project.format(
4576 HashSet::from_iter([buffer_b.clone()]),
4577 LspFormatTarget::Buffers,
4578 true,
4579 FormatTrigger::Save,
4580 cx,
4581 )
4582 })
4583 .await
4584 .unwrap();
4585
4586 // The edits from the LSP are applied, and a final newline is added.
4587 assert_eq!(
4588 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4589 "let honey = \"two\"\n"
4590 );
4591
4592 // There is no `awk` command on Windows.
4593 #[cfg(not(target_os = "windows"))]
4594 {
4595 // Ensure buffer can be formatted using an external command. Notice how the
4596 // host's configuration is honored as opposed to using the guest's settings.
4597 cx_a.update(|cx| {
4598 SettingsStore::update_global(cx, |store, cx| {
4599 store.update_user_settings(cx, |file| {
4600 file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
4601 FormatterList::Single(Formatter::External {
4602 command: "awk".into(),
4603 arguments: Some(
4604 vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
4605 ),
4606 }),
4607 ));
4608 });
4609 });
4610 });
4611
4612 executor.allow_parking();
4613 project_b
4614 .update(cx_b, |project, cx| {
4615 project.format(
4616 HashSet::from_iter([buffer_b.clone()]),
4617 LspFormatTarget::Buffers,
4618 true,
4619 FormatTrigger::Save,
4620 cx,
4621 )
4622 })
4623 .await
4624 .unwrap();
4625 assert_eq!(
4626 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4627 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4628 );
4629 }
4630}
4631
4632#[gpui::test(iterations = 10)]
4633async fn test_prettier_formatting_buffer(
4634 executor: BackgroundExecutor,
4635 cx_a: &mut TestAppContext,
4636 cx_b: &mut TestAppContext,
4637) {
4638 let mut server = TestServer::start(executor.clone()).await;
4639 let client_a = server.create_client(cx_a, "user_a").await;
4640 let client_b = server.create_client(cx_b, "user_b").await;
4641 server
4642 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4643 .await;
4644 let active_call_a = cx_a.read(ActiveCall::global);
4645
4646 let test_plugin = "test_plugin";
4647
4648 client_a.language_registry().add(Arc::new(Language::new(
4649 LanguageConfig {
4650 name: "TypeScript".into(),
4651 matcher: LanguageMatcher {
4652 path_suffixes: vec!["ts".to_string()],
4653 ..Default::default()
4654 },
4655 ..Default::default()
4656 },
4657 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
4658 )));
4659 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4660 "TypeScript",
4661 FakeLspAdapter {
4662 prettier_plugins: vec![test_plugin],
4663 ..Default::default()
4664 },
4665 );
4666
4667 // Here we insert a fake tree with a directory that exists on disk. This is needed
4668 // because later we'll invoke a command, which requires passing a working directory
4669 // that points to a valid location on disk.
4670 let directory = env::current_dir().unwrap();
4671 let buffer_text = "let one = \"two\"";
4672 client_a
4673 .fs()
4674 .insert_tree(&directory, json!({ "a.ts": buffer_text }))
4675 .await;
4676 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4677 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
4678 let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
4679 let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
4680
4681 let project_id = active_call_a
4682 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4683 .await
4684 .unwrap();
4685 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4686 let (buffer_b, _) = project_b
4687 .update(cx_b, |p, cx| {
4688 p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
4689 })
4690 .await
4691 .unwrap();
4692
4693 cx_a.update(|cx| {
4694 SettingsStore::update_global(cx, |store, cx| {
4695 store.update_user_settings(cx, |file| {
4696 file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
4697 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
4698 allowed: Some(true),
4699 ..Default::default()
4700 });
4701 });
4702 });
4703 });
4704 cx_b.update(|cx| {
4705 SettingsStore::update_global(cx, |store, cx| {
4706 store.update_user_settings(cx, |file| {
4707 file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
4708 FormatterList::Single(Formatter::LanguageServer { name: None }),
4709 ));
4710 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
4711 allowed: Some(true),
4712 ..Default::default()
4713 });
4714 });
4715 });
4716 });
4717 let fake_language_server = fake_language_servers.next().await.unwrap();
4718 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(|_, _| async move {
4719 panic!(
4720 "Unexpected: prettier should be preferred since it's enabled and language supports it"
4721 )
4722 });
4723
4724 project_b
4725 .update(cx_b, |project, cx| {
4726 project.format(
4727 HashSet::from_iter([buffer_b.clone()]),
4728 LspFormatTarget::Buffers,
4729 true,
4730 FormatTrigger::Save,
4731 cx,
4732 )
4733 })
4734 .await
4735 .unwrap();
4736
4737 executor.run_until_parked();
4738 assert_eq!(
4739 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4740 buffer_text.to_string() + "\n" + prettier_format_suffix,
4741 "Prettier formatting was not applied to client buffer after client's request"
4742 );
4743
4744 project_a
4745 .update(cx_a, |project, cx| {
4746 project.format(
4747 HashSet::from_iter([buffer_a.clone()]),
4748 LspFormatTarget::Buffers,
4749 true,
4750 FormatTrigger::Manual,
4751 cx,
4752 )
4753 })
4754 .await
4755 .unwrap();
4756
4757 executor.run_until_parked();
4758 assert_eq!(
4759 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4760 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
4761 "Prettier formatting was not applied to client buffer after host's request"
4762 );
4763}
4764
4765#[gpui::test(iterations = 10)]
4766async fn test_definition(
4767 executor: BackgroundExecutor,
4768 cx_a: &mut TestAppContext,
4769 cx_b: &mut TestAppContext,
4770) {
4771 let mut server = TestServer::start(executor.clone()).await;
4772 let client_a = server.create_client(cx_a, "user_a").await;
4773 let client_b = server.create_client(cx_b, "user_b").await;
4774 server
4775 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4776 .await;
4777 let active_call_a = cx_a.read(ActiveCall::global);
4778
4779 let capabilities = lsp::ServerCapabilities {
4780 definition_provider: Some(OneOf::Left(true)),
4781 type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
4782 ..lsp::ServerCapabilities::default()
4783 };
4784 client_a.language_registry().add(rust_lang());
4785 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4786 "Rust",
4787 FakeLspAdapter {
4788 capabilities: capabilities.clone(),
4789 ..FakeLspAdapter::default()
4790 },
4791 );
4792 client_b.language_registry().add(rust_lang());
4793 client_b.language_registry().register_fake_lsp_adapter(
4794 "Rust",
4795 FakeLspAdapter {
4796 capabilities,
4797 ..FakeLspAdapter::default()
4798 },
4799 );
4800
4801 client_a
4802 .fs()
4803 .insert_tree(
4804 path!("/root"),
4805 json!({
4806 "dir-1": {
4807 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4808 },
4809 "dir-2": {
4810 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4811 "c.rs": "type T2 = usize;",
4812 }
4813 }),
4814 )
4815 .await;
4816 let (project_a, worktree_id) = client_a
4817 .build_local_project(path!("/root/dir-1"), cx_a)
4818 .await;
4819 let project_id = active_call_a
4820 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4821 .await
4822 .unwrap();
4823 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4824
4825 // Open the file on client B.
4826 let (buffer_b, _handle) = project_b
4827 .update(cx_b, |p, cx| {
4828 p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
4829 })
4830 .await
4831 .unwrap();
4832
4833 // Request the definition of a symbol as the guest.
4834 let fake_language_server = fake_language_servers.next().await.unwrap();
4835 fake_language_server.set_request_handler::<lsp::request::GotoDefinition, _, _>(
4836 |_, _| async move {
4837 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4838 lsp::Location::new(
4839 lsp::Uri::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
4840 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4841 ),
4842 )))
4843 },
4844 );
4845 cx_a.run_until_parked();
4846 cx_b.run_until_parked();
4847
4848 let definitions_1 = project_b
4849 .update(cx_b, |p, cx| p.definitions(&buffer_b, 23, cx))
4850 .await
4851 .unwrap()
4852 .unwrap();
4853 cx_b.read(|cx| {
4854 assert_eq!(
4855 definitions_1.len(),
4856 1,
4857 "Unexpected definitions: {definitions_1:?}"
4858 );
4859 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4860 let target_buffer = definitions_1[0].target.buffer.read(cx);
4861 assert_eq!(
4862 target_buffer.text(),
4863 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4864 );
4865 assert_eq!(
4866 definitions_1[0].target.range.to_point(target_buffer),
4867 Point::new(0, 6)..Point::new(0, 9)
4868 );
4869 });
4870
4871 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4872 // the previous call to `definition`.
4873 fake_language_server.set_request_handler::<lsp::request::GotoDefinition, _, _>(
4874 |_, _| async move {
4875 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4876 lsp::Location::new(
4877 lsp::Uri::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
4878 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4879 ),
4880 )))
4881 },
4882 );
4883
4884 let definitions_2 = project_b
4885 .update(cx_b, |p, cx| p.definitions(&buffer_b, 33, cx))
4886 .await
4887 .unwrap()
4888 .unwrap();
4889 cx_b.read(|cx| {
4890 assert_eq!(definitions_2.len(), 1);
4891 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4892 let target_buffer = definitions_2[0].target.buffer.read(cx);
4893 assert_eq!(
4894 target_buffer.text(),
4895 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4896 );
4897 assert_eq!(
4898 definitions_2[0].target.range.to_point(target_buffer),
4899 Point::new(1, 6)..Point::new(1, 11)
4900 );
4901 });
4902 assert_eq!(
4903 definitions_1[0].target.buffer,
4904 definitions_2[0].target.buffer
4905 );
4906
4907 fake_language_server.set_request_handler::<lsp::request::GotoTypeDefinition, _, _>(
4908 |req, _| async move {
4909 assert_eq!(
4910 req.text_document_position_params.position,
4911 lsp::Position::new(0, 7)
4912 );
4913 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4914 lsp::Location::new(
4915 lsp::Uri::from_file_path(path!("/root/dir-2/c.rs")).unwrap(),
4916 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4917 ),
4918 )))
4919 },
4920 );
4921
4922 let type_definitions = project_b
4923 .update(cx_b, |p, cx| p.type_definitions(&buffer_b, 7, cx))
4924 .await
4925 .unwrap()
4926 .unwrap();
4927 cx_b.read(|cx| {
4928 assert_eq!(
4929 type_definitions.len(),
4930 1,
4931 "Unexpected type definitions: {type_definitions:?}"
4932 );
4933 let target_buffer = type_definitions[0].target.buffer.read(cx);
4934 assert_eq!(target_buffer.text(), "type T2 = usize;");
4935 assert_eq!(
4936 type_definitions[0].target.range.to_point(target_buffer),
4937 Point::new(0, 5)..Point::new(0, 7)
4938 );
4939 });
4940}
4941
4942#[gpui::test(iterations = 10)]
4943async fn test_references(
4944 executor: BackgroundExecutor,
4945 cx_a: &mut TestAppContext,
4946 cx_b: &mut TestAppContext,
4947) {
4948 let mut server = TestServer::start(executor.clone()).await;
4949 let client_a = server.create_client(cx_a, "user_a").await;
4950 let client_b = server.create_client(cx_b, "user_b").await;
4951 server
4952 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4953 .await;
4954 let active_call_a = cx_a.read(ActiveCall::global);
4955
4956 let capabilities = lsp::ServerCapabilities {
4957 references_provider: Some(lsp::OneOf::Left(true)),
4958 ..lsp::ServerCapabilities::default()
4959 };
4960 client_a.language_registry().add(rust_lang());
4961 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4962 "Rust",
4963 FakeLspAdapter {
4964 name: "my-fake-lsp-adapter",
4965 capabilities: capabilities.clone(),
4966 ..FakeLspAdapter::default()
4967 },
4968 );
4969 client_b.language_registry().add(rust_lang());
4970 client_b.language_registry().register_fake_lsp_adapter(
4971 "Rust",
4972 FakeLspAdapter {
4973 name: "my-fake-lsp-adapter",
4974 capabilities,
4975 ..FakeLspAdapter::default()
4976 },
4977 );
4978
4979 client_a
4980 .fs()
4981 .insert_tree(
4982 path!("/root"),
4983 json!({
4984 "dir-1": {
4985 "one.rs": "const ONE: usize = 1;",
4986 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4987 },
4988 "dir-2": {
4989 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4990 }
4991 }),
4992 )
4993 .await;
4994 let (project_a, worktree_id) = client_a
4995 .build_local_project(path!("/root/dir-1"), cx_a)
4996 .await;
4997 let project_id = active_call_a
4998 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4999 .await
5000 .unwrap();
5001 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5002
5003 // Open the file on client B.
5004 let (buffer_b, _handle) = project_b
5005 .update(cx_b, |p, cx| {
5006 p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
5007 })
5008 .await
5009 .unwrap();
5010
5011 // Request references to a symbol as the guest.
5012 let fake_language_server = fake_language_servers.next().await.unwrap();
5013 let (lsp_response_tx, rx) = mpsc::unbounded::<Result<Option<Vec<lsp::Location>>>>();
5014 fake_language_server.set_request_handler::<lsp::request::References, _, _>({
5015 let rx = Arc::new(Mutex::new(Some(rx)));
5016 move |params, _| {
5017 assert_eq!(
5018 params.text_document_position.text_document.uri.as_str(),
5019 uri!("file:///root/dir-1/one.rs")
5020 );
5021 let rx = rx.clone();
5022 async move {
5023 let mut response_rx = rx.lock().take().unwrap();
5024 let result = response_rx.next().await.unwrap();
5025 *rx.lock() = Some(response_rx);
5026 result
5027 }
5028 }
5029 });
5030 cx_a.run_until_parked();
5031 cx_b.run_until_parked();
5032
5033 let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx));
5034
5035 // User is informed that a request is pending.
5036 executor.run_until_parked();
5037 project_b.read_with(cx_b, |project, cx| {
5038 let status = project.language_server_statuses(cx).next().unwrap().1;
5039 assert_eq!(status.name.0, "my-fake-lsp-adapter");
5040 assert_eq!(
5041 status.pending_work.values().next().unwrap().message,
5042 Some("Finding references...".into())
5043 );
5044 });
5045
5046 // Cause the language server to respond.
5047 lsp_response_tx
5048 .unbounded_send(Ok(Some(vec![
5049 lsp::Location {
5050 uri: lsp::Uri::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
5051 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
5052 },
5053 lsp::Location {
5054 uri: lsp::Uri::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
5055 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
5056 },
5057 lsp::Location {
5058 uri: lsp::Uri::from_file_path(path!("/root/dir-2/three.rs")).unwrap(),
5059 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
5060 },
5061 ])))
5062 .unwrap();
5063
5064 let references = references.await.unwrap().unwrap();
5065 executor.run_until_parked();
5066 project_b.read_with(cx_b, |project, cx| {
5067 // User is informed that a request is no longer pending.
5068 let status = project.language_server_statuses(cx).next().unwrap().1;
5069 assert!(status.pending_work.is_empty());
5070
5071 assert_eq!(references.len(), 3);
5072 assert_eq!(project.worktrees(cx).count(), 2);
5073
5074 let two_buffer = references[0].buffer.read(cx);
5075 let three_buffer = references[2].buffer.read(cx);
5076 assert_eq!(
5077 two_buffer.file().unwrap().path().as_ref(),
5078 Path::new("two.rs")
5079 );
5080 assert_eq!(references[1].buffer, references[0].buffer);
5081 assert_eq!(
5082 three_buffer.file().unwrap().full_path(cx),
5083 Path::new(path!("/root/dir-2/three.rs"))
5084 );
5085
5086 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
5087 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
5088 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
5089 });
5090
5091 let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx));
5092
5093 // User is informed that a request is pending.
5094 executor.run_until_parked();
5095 project_b.read_with(cx_b, |project, cx| {
5096 let status = project.language_server_statuses(cx).next().unwrap().1;
5097 assert_eq!(status.name.0, "my-fake-lsp-adapter");
5098 assert_eq!(
5099 status.pending_work.values().next().unwrap().message,
5100 Some("Finding references...".into())
5101 );
5102 });
5103
5104 // Cause the LSP request to fail.
5105 lsp_response_tx
5106 .unbounded_send(Err(anyhow!("can't find references")))
5107 .unwrap();
5108 assert_eq!(references.await.unwrap().unwrap(), []);
5109
5110 // User is informed that the request is no longer pending.
5111 executor.run_until_parked();
5112 project_b.read_with(cx_b, |project, cx| {
5113 let status = project.language_server_statuses(cx).next().unwrap().1;
5114 assert!(status.pending_work.is_empty());
5115 });
5116}
5117
5118#[gpui::test(iterations = 10)]
5119async fn test_project_search(
5120 executor: BackgroundExecutor,
5121 cx_a: &mut TestAppContext,
5122 cx_b: &mut TestAppContext,
5123) {
5124 let mut server = TestServer::start(executor.clone()).await;
5125 let client_a = server.create_client(cx_a, "user_a").await;
5126 let client_b = server.create_client(cx_b, "user_b").await;
5127 server
5128 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5129 .await;
5130 let active_call_a = cx_a.read(ActiveCall::global);
5131
5132 client_a
5133 .fs()
5134 .insert_tree(
5135 "/root",
5136 json!({
5137 "dir-1": {
5138 "a": "hello world",
5139 "b": "goodnight moon",
5140 "c": "a world of goo",
5141 "d": "world champion of clown world",
5142 },
5143 "dir-2": {
5144 "e": "disney world is fun",
5145 }
5146 }),
5147 )
5148 .await;
5149 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
5150 let (worktree_2, _) = project_a
5151 .update(cx_a, |p, cx| {
5152 p.find_or_create_worktree("/root/dir-2", true, cx)
5153 })
5154 .await
5155 .unwrap();
5156 worktree_2
5157 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
5158 .await;
5159 let project_id = active_call_a
5160 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5161 .await
5162 .unwrap();
5163
5164 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5165
5166 // Perform a search as the guest.
5167 let mut results = HashMap::default();
5168 let search_rx = project_b.update(cx_b, |project, cx| {
5169 project.search(
5170 SearchQuery::text(
5171 "world",
5172 false,
5173 false,
5174 false,
5175 Default::default(),
5176 Default::default(),
5177 false,
5178 None,
5179 )
5180 .unwrap(),
5181 cx,
5182 )
5183 });
5184 while let Ok(result) = search_rx.recv().await {
5185 match result {
5186 SearchResult::Buffer { buffer, ranges } => {
5187 results.entry(buffer).or_insert(ranges);
5188 }
5189 SearchResult::LimitReached => {
5190 panic!(
5191 "Unexpectedly reached search limit in tests. If you do want to assert limit-reached, change this panic call."
5192 )
5193 }
5194 };
5195 }
5196
5197 let mut ranges_by_path = results
5198 .into_iter()
5199 .map(|(buffer, ranges)| {
5200 buffer.read_with(cx_b, |buffer, cx| {
5201 let path = buffer.file().unwrap().full_path(cx);
5202 let offset_ranges = ranges
5203 .into_iter()
5204 .map(|range| range.to_offset(buffer))
5205 .collect::<Vec<_>>();
5206 (path, offset_ranges)
5207 })
5208 })
5209 .collect::<Vec<_>>();
5210 ranges_by_path.sort_by_key(|(path, _)| path.clone());
5211
5212 assert_eq!(
5213 ranges_by_path,
5214 &[
5215 (PathBuf::from("dir-1/a"), vec![6..11]),
5216 (PathBuf::from("dir-1/c"), vec![2..7]),
5217 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
5218 (PathBuf::from("dir-2/e"), vec![7..12]),
5219 ]
5220 );
5221}
5222
5223#[gpui::test(iterations = 10)]
5224async fn test_document_highlights(
5225 executor: BackgroundExecutor,
5226 cx_a: &mut TestAppContext,
5227 cx_b: &mut TestAppContext,
5228) {
5229 let mut server = TestServer::start(executor.clone()).await;
5230 let client_a = server.create_client(cx_a, "user_a").await;
5231 let client_b = server.create_client(cx_b, "user_b").await;
5232 server
5233 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5234 .await;
5235 let active_call_a = cx_a.read(ActiveCall::global);
5236
5237 client_a
5238 .fs()
5239 .insert_tree(
5240 path!("/root-1"),
5241 json!({
5242 "main.rs": "fn double(number: i32) -> i32 { number + number }",
5243 }),
5244 )
5245 .await;
5246
5247 client_a.language_registry().add(rust_lang());
5248 let capabilities = lsp::ServerCapabilities {
5249 document_highlight_provider: Some(lsp::OneOf::Left(true)),
5250 ..lsp::ServerCapabilities::default()
5251 };
5252 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
5253 "Rust",
5254 FakeLspAdapter {
5255 capabilities: capabilities.clone(),
5256 ..FakeLspAdapter::default()
5257 },
5258 );
5259 client_b.language_registry().add(rust_lang());
5260 client_b.language_registry().register_fake_lsp_adapter(
5261 "Rust",
5262 FakeLspAdapter {
5263 capabilities,
5264 ..FakeLspAdapter::default()
5265 },
5266 );
5267
5268 let (project_a, worktree_id) = client_a.build_local_project(path!("/root-1"), cx_a).await;
5269 let project_id = active_call_a
5270 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5271 .await
5272 .unwrap();
5273 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5274
5275 // Open the file on client B.
5276 let (buffer_b, _handle) = project_b
5277 .update(cx_b, |p, cx| {
5278 p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
5279 })
5280 .await
5281 .unwrap();
5282
5283 // Request document highlights as the guest.
5284 let fake_language_server = fake_language_servers.next().await.unwrap();
5285 fake_language_server.set_request_handler::<lsp::request::DocumentHighlightRequest, _, _>(
5286 |params, _| async move {
5287 assert_eq!(
5288 params
5289 .text_document_position_params
5290 .text_document
5291 .uri
5292 .as_str(),
5293 uri!("file:///root-1/main.rs")
5294 );
5295 assert_eq!(
5296 params.text_document_position_params.position,
5297 lsp::Position::new(0, 34)
5298 );
5299 Ok(Some(vec![
5300 lsp::DocumentHighlight {
5301 kind: Some(lsp::DocumentHighlightKind::WRITE),
5302 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
5303 },
5304 lsp::DocumentHighlight {
5305 kind: Some(lsp::DocumentHighlightKind::READ),
5306 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
5307 },
5308 lsp::DocumentHighlight {
5309 kind: Some(lsp::DocumentHighlightKind::READ),
5310 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
5311 },
5312 ]))
5313 },
5314 );
5315 cx_a.run_until_parked();
5316 cx_b.run_until_parked();
5317
5318 let highlights = project_b
5319 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
5320 .await
5321 .unwrap();
5322
5323 buffer_b.read_with(cx_b, |buffer, _| {
5324 let snapshot = buffer.snapshot();
5325
5326 let highlights = highlights
5327 .into_iter()
5328 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
5329 .collect::<Vec<_>>();
5330 assert_eq!(
5331 highlights,
5332 &[
5333 (lsp::DocumentHighlightKind::WRITE, 10..16),
5334 (lsp::DocumentHighlightKind::READ, 32..38),
5335 (lsp::DocumentHighlightKind::READ, 41..47)
5336 ]
5337 )
5338 });
5339}
5340
5341#[gpui::test(iterations = 10)]
5342async fn test_lsp_hover(
5343 executor: BackgroundExecutor,
5344 cx_a: &mut TestAppContext,
5345 cx_b: &mut TestAppContext,
5346) {
5347 let mut server = TestServer::start(executor.clone()).await;
5348 let client_a = server.create_client(cx_a, "user_a").await;
5349 let client_b = server.create_client(cx_b, "user_b").await;
5350 server
5351 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5352 .await;
5353 let active_call_a = cx_a.read(ActiveCall::global);
5354
5355 client_a
5356 .fs()
5357 .insert_tree(
5358 path!("/root-1"),
5359 json!({
5360 "main.rs": "use std::collections::HashMap;",
5361 }),
5362 )
5363 .await;
5364
5365 client_a.language_registry().add(rust_lang());
5366 let language_server_names = ["rust-analyzer", "CrabLang-ls"];
5367 let capabilities_1 = lsp::ServerCapabilities {
5368 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5369 ..lsp::ServerCapabilities::default()
5370 };
5371 let capabilities_2 = lsp::ServerCapabilities {
5372 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5373 ..lsp::ServerCapabilities::default()
5374 };
5375 let mut language_servers = [
5376 client_a.language_registry().register_fake_lsp(
5377 "Rust",
5378 FakeLspAdapter {
5379 name: language_server_names[0],
5380 capabilities: capabilities_1.clone(),
5381 ..FakeLspAdapter::default()
5382 },
5383 ),
5384 client_a.language_registry().register_fake_lsp(
5385 "Rust",
5386 FakeLspAdapter {
5387 name: language_server_names[1],
5388 capabilities: capabilities_2.clone(),
5389 ..FakeLspAdapter::default()
5390 },
5391 ),
5392 ];
5393 client_b.language_registry().add(rust_lang());
5394 client_b.language_registry().register_fake_lsp_adapter(
5395 "Rust",
5396 FakeLspAdapter {
5397 name: language_server_names[0],
5398 capabilities: capabilities_1,
5399 ..FakeLspAdapter::default()
5400 },
5401 );
5402 client_b.language_registry().register_fake_lsp_adapter(
5403 "Rust",
5404 FakeLspAdapter {
5405 name: language_server_names[1],
5406 capabilities: capabilities_2,
5407 ..FakeLspAdapter::default()
5408 },
5409 );
5410
5411 let (project_a, worktree_id) = client_a.build_local_project(path!("/root-1"), cx_a).await;
5412 let project_id = active_call_a
5413 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5414 .await
5415 .unwrap();
5416 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5417
5418 // Open the file as the guest
5419 let (buffer_b, _handle) = project_b
5420 .update(cx_b, |p, cx| {
5421 p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
5422 })
5423 .await
5424 .unwrap();
5425
5426 let mut servers_with_hover_requests = HashMap::default();
5427 for i in 0..language_server_names.len() {
5428 let new_server = language_servers[i].next().await.unwrap_or_else(|| {
5429 panic!(
5430 "Failed to get language server #{i} with name {}",
5431 &language_server_names[i]
5432 )
5433 });
5434 let new_server_name = new_server.server.name();
5435 assert!(
5436 !servers_with_hover_requests.contains_key(&new_server_name),
5437 "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
5438 );
5439 match new_server_name.as_ref() {
5440 "CrabLang-ls" => {
5441 servers_with_hover_requests.insert(
5442 new_server_name.clone(),
5443 new_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
5444 move |params, _| {
5445 assert_eq!(
5446 params
5447 .text_document_position_params
5448 .text_document
5449 .uri
5450 .as_str(),
5451 uri!("file:///root-1/main.rs")
5452 );
5453 let name = new_server_name.clone();
5454 async move {
5455 Ok(Some(lsp::Hover {
5456 contents: lsp::HoverContents::Scalar(
5457 lsp::MarkedString::String(format!("{name} hover")),
5458 ),
5459 range: None,
5460 }))
5461 }
5462 },
5463 ),
5464 );
5465 }
5466 "rust-analyzer" => {
5467 servers_with_hover_requests.insert(
5468 new_server_name.clone(),
5469 new_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
5470 |params, _| async move {
5471 assert_eq!(
5472 params
5473 .text_document_position_params
5474 .text_document
5475 .uri
5476 .as_str(),
5477 uri!("file:///root-1/main.rs")
5478 );
5479 assert_eq!(
5480 params.text_document_position_params.position,
5481 lsp::Position::new(0, 22)
5482 );
5483 Ok(Some(lsp::Hover {
5484 contents: lsp::HoverContents::Array(vec![
5485 lsp::MarkedString::String("Test hover content.".to_string()),
5486 lsp::MarkedString::LanguageString(lsp::LanguageString {
5487 language: "Rust".to_string(),
5488 value: "let foo = 42;".to_string(),
5489 }),
5490 ]),
5491 range: Some(lsp::Range::new(
5492 lsp::Position::new(0, 22),
5493 lsp::Position::new(0, 29),
5494 )),
5495 }))
5496 },
5497 ),
5498 );
5499 }
5500 unexpected => panic!("Unexpected server name: {unexpected}"),
5501 }
5502 }
5503 cx_a.run_until_parked();
5504 cx_b.run_until_parked();
5505
5506 // Request hover information as the guest.
5507 let mut hovers = project_b
5508 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
5509 .await
5510 .unwrap();
5511 assert_eq!(
5512 hovers.len(),
5513 2,
5514 "Expected two hovers from both language servers, but got: {hovers:?}"
5515 );
5516
5517 let _: Vec<()> = futures::future::join_all(servers_with_hover_requests.into_values().map(
5518 |mut hover_request| async move {
5519 hover_request
5520 .next()
5521 .await
5522 .expect("All hover requests should have been triggered")
5523 },
5524 ))
5525 .await;
5526
5527 hovers.sort_by_key(|hover| hover.contents.len());
5528 let first_hover = hovers.first().cloned().unwrap();
5529 assert_eq!(
5530 first_hover.contents,
5531 vec![project::HoverBlock {
5532 text: "CrabLang-ls hover".to_string(),
5533 kind: HoverBlockKind::Markdown,
5534 },]
5535 );
5536 let second_hover = hovers.last().cloned().unwrap();
5537 assert_eq!(
5538 second_hover.contents,
5539 vec![
5540 project::HoverBlock {
5541 text: "Test hover content.".to_string(),
5542 kind: HoverBlockKind::Markdown,
5543 },
5544 project::HoverBlock {
5545 text: "let foo = 42;".to_string(),
5546 kind: HoverBlockKind::Code {
5547 language: "Rust".to_string()
5548 },
5549 }
5550 ]
5551 );
5552 buffer_b.read_with(cx_b, |buffer, _| {
5553 let snapshot = buffer.snapshot();
5554 assert_eq!(second_hover.range.unwrap().to_offset(&snapshot), 22..29);
5555 });
5556}
5557
5558#[gpui::test(iterations = 10)]
5559async fn test_project_symbols(
5560 executor: BackgroundExecutor,
5561 cx_a: &mut TestAppContext,
5562 cx_b: &mut TestAppContext,
5563) {
5564 let mut server = TestServer::start(executor.clone()).await;
5565 let client_a = server.create_client(cx_a, "user_a").await;
5566 let client_b = server.create_client(cx_b, "user_b").await;
5567 server
5568 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5569 .await;
5570 let active_call_a = cx_a.read(ActiveCall::global);
5571
5572 client_a.language_registry().add(rust_lang());
5573 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
5574 "Rust",
5575 FakeLspAdapter {
5576 capabilities: lsp::ServerCapabilities {
5577 workspace_symbol_provider: Some(OneOf::Left(true)),
5578 ..Default::default()
5579 },
5580 ..Default::default()
5581 },
5582 );
5583
5584 client_a
5585 .fs()
5586 .insert_tree(
5587 path!("/code"),
5588 json!({
5589 "crate-1": {
5590 "one.rs": "const ONE: usize = 1;",
5591 },
5592 "crate-2": {
5593 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
5594 },
5595 "private": {
5596 "passwords.txt": "the-password",
5597 }
5598 }),
5599 )
5600 .await;
5601 let (project_a, worktree_id) = client_a
5602 .build_local_project(path!("/code/crate-1"), cx_a)
5603 .await;
5604 let project_id = active_call_a
5605 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5606 .await
5607 .unwrap();
5608 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5609
5610 // Cause the language server to start.
5611 let _buffer = project_b
5612 .update(cx_b, |p, cx| {
5613 p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
5614 })
5615 .await
5616 .unwrap();
5617
5618 let fake_language_server = fake_language_servers.next().await.unwrap();
5619 fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
5620 |_, _| async move {
5621 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
5622 #[allow(deprecated)]
5623 lsp::SymbolInformation {
5624 name: "TWO".into(),
5625 location: lsp::Location {
5626 uri: lsp::Uri::from_file_path(path!("/code/crate-2/two.rs")).unwrap(),
5627 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5628 },
5629 kind: lsp::SymbolKind::CONSTANT,
5630 tags: None,
5631 container_name: None,
5632 deprecated: None,
5633 },
5634 ])))
5635 },
5636 );
5637
5638 // Request the definition of a symbol as the guest.
5639 let symbols = project_b
5640 .update(cx_b, |p, cx| p.symbols("two", cx))
5641 .await
5642 .unwrap();
5643 assert_eq!(symbols.len(), 1);
5644 assert_eq!(symbols[0].name, "TWO");
5645
5646 // Open one of the returned symbols.
5647 let buffer_b_2 = project_b
5648 .update(cx_b, |project, cx| {
5649 project.open_buffer_for_symbol(&symbols[0], cx)
5650 })
5651 .await
5652 .unwrap();
5653
5654 buffer_b_2.read_with(cx_b, |buffer, cx| {
5655 assert_eq!(
5656 buffer.file().unwrap().full_path(cx),
5657 Path::new(path!("/code/crate-2/two.rs"))
5658 );
5659 });
5660
5661 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
5662 let mut fake_symbol = symbols[0].clone();
5663 fake_symbol.path.path = Path::new(path!("/code/secrets")).into();
5664 let error = project_b
5665 .update(cx_b, |project, cx| {
5666 project.open_buffer_for_symbol(&fake_symbol, cx)
5667 })
5668 .await
5669 .unwrap_err();
5670 assert!(error.to_string().contains("invalid symbol signature"));
5671}
5672
5673#[gpui::test(iterations = 10)]
5674async fn test_open_buffer_while_getting_definition_pointing_to_it(
5675 executor: BackgroundExecutor,
5676 cx_a: &mut TestAppContext,
5677 cx_b: &mut TestAppContext,
5678 mut rng: StdRng,
5679) {
5680 let mut server = TestServer::start(executor.clone()).await;
5681 let client_a = server.create_client(cx_a, "user_a").await;
5682 let client_b = server.create_client(cx_b, "user_b").await;
5683 server
5684 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5685 .await;
5686 let active_call_a = cx_a.read(ActiveCall::global);
5687
5688 let capabilities = lsp::ServerCapabilities {
5689 definition_provider: Some(OneOf::Left(true)),
5690 ..lsp::ServerCapabilities::default()
5691 };
5692 client_a.language_registry().add(rust_lang());
5693 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
5694 "Rust",
5695 FakeLspAdapter {
5696 capabilities: capabilities.clone(),
5697 ..FakeLspAdapter::default()
5698 },
5699 );
5700 client_b.language_registry().add(rust_lang());
5701 client_b.language_registry().register_fake_lsp_adapter(
5702 "Rust",
5703 FakeLspAdapter {
5704 capabilities,
5705 ..FakeLspAdapter::default()
5706 },
5707 );
5708
5709 client_a
5710 .fs()
5711 .insert_tree(
5712 path!("/root"),
5713 json!({
5714 "a.rs": "const ONE: usize = b::TWO;",
5715 "b.rs": "const TWO: usize = 2",
5716 }),
5717 )
5718 .await;
5719 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
5720 let project_id = active_call_a
5721 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5722 .await
5723 .unwrap();
5724 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5725
5726 let (buffer_b1, _lsp) = project_b
5727 .update(cx_b, |p, cx| {
5728 p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
5729 })
5730 .await
5731 .unwrap();
5732
5733 let fake_language_server = fake_language_servers.next().await.unwrap();
5734 fake_language_server.set_request_handler::<lsp::request::GotoDefinition, _, _>(
5735 |_, _| async move {
5736 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
5737 lsp::Location::new(
5738 lsp::Uri::from_file_path(path!("/root/b.rs")).unwrap(),
5739 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5740 ),
5741 )))
5742 },
5743 );
5744
5745 let definitions;
5746 let buffer_b2;
5747 if rng.random() {
5748 cx_a.run_until_parked();
5749 cx_b.run_until_parked();
5750 definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx));
5751 (buffer_b2, _) = project_b
5752 .update(cx_b, |p, cx| {
5753 p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
5754 })
5755 .await
5756 .unwrap();
5757 } else {
5758 (buffer_b2, _) = project_b
5759 .update(cx_b, |p, cx| {
5760 p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
5761 })
5762 .await
5763 .unwrap();
5764 cx_a.run_until_parked();
5765 cx_b.run_until_parked();
5766 definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx));
5767 }
5768
5769 let definitions = definitions.await.unwrap().unwrap();
5770 assert_eq!(
5771 definitions.len(),
5772 1,
5773 "Unexpected definitions: {definitions:?}"
5774 );
5775 assert_eq!(definitions[0].target.buffer, buffer_b2);
5776}
5777
5778#[gpui::test(iterations = 10)]
5779async fn test_contacts(
5780 executor: BackgroundExecutor,
5781 cx_a: &mut TestAppContext,
5782 cx_b: &mut TestAppContext,
5783 cx_c: &mut TestAppContext,
5784 cx_d: &mut TestAppContext,
5785) {
5786 let mut server = TestServer::start(executor.clone()).await;
5787 let client_a = server.create_client(cx_a, "user_a").await;
5788 let client_b = server.create_client(cx_b, "user_b").await;
5789 let client_c = server.create_client(cx_c, "user_c").await;
5790 let client_d = server.create_client(cx_d, "user_d").await;
5791 server
5792 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5793 .await;
5794 let active_call_a = cx_a.read(ActiveCall::global);
5795 let active_call_b = cx_b.read(ActiveCall::global);
5796 let active_call_c = cx_c.read(ActiveCall::global);
5797 let _active_call_d = cx_d.read(ActiveCall::global);
5798
5799 executor.run_until_parked();
5800 assert_eq!(
5801 contacts(&client_a, cx_a),
5802 [
5803 ("user_b".to_string(), "online", "free"),
5804 ("user_c".to_string(), "online", "free")
5805 ]
5806 );
5807 assert_eq!(
5808 contacts(&client_b, cx_b),
5809 [
5810 ("user_a".to_string(), "online", "free"),
5811 ("user_c".to_string(), "online", "free")
5812 ]
5813 );
5814 assert_eq!(
5815 contacts(&client_c, cx_c),
5816 [
5817 ("user_a".to_string(), "online", "free"),
5818 ("user_b".to_string(), "online", "free")
5819 ]
5820 );
5821 assert_eq!(contacts(&client_d, cx_d), []);
5822
5823 server.disconnect_client(client_c.peer_id().unwrap());
5824 server.forbid_connections();
5825 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5826 assert_eq!(
5827 contacts(&client_a, cx_a),
5828 [
5829 ("user_b".to_string(), "online", "free"),
5830 ("user_c".to_string(), "offline", "free")
5831 ]
5832 );
5833 assert_eq!(
5834 contacts(&client_b, cx_b),
5835 [
5836 ("user_a".to_string(), "online", "free"),
5837 ("user_c".to_string(), "offline", "free")
5838 ]
5839 );
5840 assert_eq!(contacts(&client_c, cx_c), []);
5841 assert_eq!(contacts(&client_d, cx_d), []);
5842
5843 server.allow_connections();
5844 client_c
5845 .connect(false, &cx_c.to_async())
5846 .await
5847 .into_response()
5848 .unwrap();
5849
5850 executor.run_until_parked();
5851 assert_eq!(
5852 contacts(&client_a, cx_a),
5853 [
5854 ("user_b".to_string(), "online", "free"),
5855 ("user_c".to_string(), "online", "free")
5856 ]
5857 );
5858 assert_eq!(
5859 contacts(&client_b, cx_b),
5860 [
5861 ("user_a".to_string(), "online", "free"),
5862 ("user_c".to_string(), "online", "free")
5863 ]
5864 );
5865 assert_eq!(
5866 contacts(&client_c, cx_c),
5867 [
5868 ("user_a".to_string(), "online", "free"),
5869 ("user_b".to_string(), "online", "free")
5870 ]
5871 );
5872 assert_eq!(contacts(&client_d, cx_d), []);
5873
5874 active_call_a
5875 .update(cx_a, |call, cx| {
5876 call.invite(client_b.user_id().unwrap(), None, cx)
5877 })
5878 .await
5879 .unwrap();
5880 executor.run_until_parked();
5881 assert_eq!(
5882 contacts(&client_a, cx_a),
5883 [
5884 ("user_b".to_string(), "online", "busy"),
5885 ("user_c".to_string(), "online", "free")
5886 ]
5887 );
5888 assert_eq!(
5889 contacts(&client_b, cx_b),
5890 [
5891 ("user_a".to_string(), "online", "busy"),
5892 ("user_c".to_string(), "online", "free")
5893 ]
5894 );
5895 assert_eq!(
5896 contacts(&client_c, cx_c),
5897 [
5898 ("user_a".to_string(), "online", "busy"),
5899 ("user_b".to_string(), "online", "busy")
5900 ]
5901 );
5902 assert_eq!(contacts(&client_d, cx_d), []);
5903
5904 // Client B and client D become contacts while client B is being called.
5905 server
5906 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5907 .await;
5908 executor.run_until_parked();
5909 assert_eq!(
5910 contacts(&client_a, cx_a),
5911 [
5912 ("user_b".to_string(), "online", "busy"),
5913 ("user_c".to_string(), "online", "free")
5914 ]
5915 );
5916 assert_eq!(
5917 contacts(&client_b, cx_b),
5918 [
5919 ("user_a".to_string(), "online", "busy"),
5920 ("user_c".to_string(), "online", "free"),
5921 ("user_d".to_string(), "online", "free"),
5922 ]
5923 );
5924 assert_eq!(
5925 contacts(&client_c, cx_c),
5926 [
5927 ("user_a".to_string(), "online", "busy"),
5928 ("user_b".to_string(), "online", "busy")
5929 ]
5930 );
5931 assert_eq!(
5932 contacts(&client_d, cx_d),
5933 [("user_b".to_string(), "online", "busy")]
5934 );
5935
5936 active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
5937 executor.run_until_parked();
5938 assert_eq!(
5939 contacts(&client_a, cx_a),
5940 [
5941 ("user_b".to_string(), "online", "free"),
5942 ("user_c".to_string(), "online", "free")
5943 ]
5944 );
5945 assert_eq!(
5946 contacts(&client_b, cx_b),
5947 [
5948 ("user_a".to_string(), "online", "free"),
5949 ("user_c".to_string(), "online", "free"),
5950 ("user_d".to_string(), "online", "free")
5951 ]
5952 );
5953 assert_eq!(
5954 contacts(&client_c, cx_c),
5955 [
5956 ("user_a".to_string(), "online", "free"),
5957 ("user_b".to_string(), "online", "free")
5958 ]
5959 );
5960 assert_eq!(
5961 contacts(&client_d, cx_d),
5962 [("user_b".to_string(), "online", "free")]
5963 );
5964
5965 active_call_c
5966 .update(cx_c, |call, cx| {
5967 call.invite(client_a.user_id().unwrap(), None, cx)
5968 })
5969 .await
5970 .unwrap();
5971 executor.run_until_parked();
5972 assert_eq!(
5973 contacts(&client_a, cx_a),
5974 [
5975 ("user_b".to_string(), "online", "free"),
5976 ("user_c".to_string(), "online", "busy")
5977 ]
5978 );
5979 assert_eq!(
5980 contacts(&client_b, cx_b),
5981 [
5982 ("user_a".to_string(), "online", "busy"),
5983 ("user_c".to_string(), "online", "busy"),
5984 ("user_d".to_string(), "online", "free")
5985 ]
5986 );
5987 assert_eq!(
5988 contacts(&client_c, cx_c),
5989 [
5990 ("user_a".to_string(), "online", "busy"),
5991 ("user_b".to_string(), "online", "free")
5992 ]
5993 );
5994 assert_eq!(
5995 contacts(&client_d, cx_d),
5996 [("user_b".to_string(), "online", "free")]
5997 );
5998
5999 active_call_a
6000 .update(cx_a, |call, cx| call.accept_incoming(cx))
6001 .await
6002 .unwrap();
6003 executor.run_until_parked();
6004 assert_eq!(
6005 contacts(&client_a, cx_a),
6006 [
6007 ("user_b".to_string(), "online", "free"),
6008 ("user_c".to_string(), "online", "busy")
6009 ]
6010 );
6011 assert_eq!(
6012 contacts(&client_b, cx_b),
6013 [
6014 ("user_a".to_string(), "online", "busy"),
6015 ("user_c".to_string(), "online", "busy"),
6016 ("user_d".to_string(), "online", "free")
6017 ]
6018 );
6019 assert_eq!(
6020 contacts(&client_c, cx_c),
6021 [
6022 ("user_a".to_string(), "online", "busy"),
6023 ("user_b".to_string(), "online", "free")
6024 ]
6025 );
6026 assert_eq!(
6027 contacts(&client_d, cx_d),
6028 [("user_b".to_string(), "online", "free")]
6029 );
6030
6031 active_call_a
6032 .update(cx_a, |call, cx| {
6033 call.invite(client_b.user_id().unwrap(), None, cx)
6034 })
6035 .await
6036 .unwrap();
6037 executor.run_until_parked();
6038 assert_eq!(
6039 contacts(&client_a, cx_a),
6040 [
6041 ("user_b".to_string(), "online", "busy"),
6042 ("user_c".to_string(), "online", "busy")
6043 ]
6044 );
6045 assert_eq!(
6046 contacts(&client_b, cx_b),
6047 [
6048 ("user_a".to_string(), "online", "busy"),
6049 ("user_c".to_string(), "online", "busy"),
6050 ("user_d".to_string(), "online", "free")
6051 ]
6052 );
6053 assert_eq!(
6054 contacts(&client_c, cx_c),
6055 [
6056 ("user_a".to_string(), "online", "busy"),
6057 ("user_b".to_string(), "online", "busy")
6058 ]
6059 );
6060 assert_eq!(
6061 contacts(&client_d, cx_d),
6062 [("user_b".to_string(), "online", "busy")]
6063 );
6064
6065 active_call_a
6066 .update(cx_a, |call, cx| call.hang_up(cx))
6067 .await
6068 .unwrap();
6069 executor.run_until_parked();
6070 assert_eq!(
6071 contacts(&client_a, cx_a),
6072 [
6073 ("user_b".to_string(), "online", "free"),
6074 ("user_c".to_string(), "online", "free")
6075 ]
6076 );
6077 assert_eq!(
6078 contacts(&client_b, cx_b),
6079 [
6080 ("user_a".to_string(), "online", "free"),
6081 ("user_c".to_string(), "online", "free"),
6082 ("user_d".to_string(), "online", "free")
6083 ]
6084 );
6085 assert_eq!(
6086 contacts(&client_c, cx_c),
6087 [
6088 ("user_a".to_string(), "online", "free"),
6089 ("user_b".to_string(), "online", "free")
6090 ]
6091 );
6092 assert_eq!(
6093 contacts(&client_d, cx_d),
6094 [("user_b".to_string(), "online", "free")]
6095 );
6096
6097 active_call_a
6098 .update(cx_a, |call, cx| {
6099 call.invite(client_b.user_id().unwrap(), None, cx)
6100 })
6101 .await
6102 .unwrap();
6103 executor.run_until_parked();
6104 assert_eq!(
6105 contacts(&client_a, cx_a),
6106 [
6107 ("user_b".to_string(), "online", "busy"),
6108 ("user_c".to_string(), "online", "free")
6109 ]
6110 );
6111 assert_eq!(
6112 contacts(&client_b, cx_b),
6113 [
6114 ("user_a".to_string(), "online", "busy"),
6115 ("user_c".to_string(), "online", "free"),
6116 ("user_d".to_string(), "online", "free")
6117 ]
6118 );
6119 assert_eq!(
6120 contacts(&client_c, cx_c),
6121 [
6122 ("user_a".to_string(), "online", "busy"),
6123 ("user_b".to_string(), "online", "busy")
6124 ]
6125 );
6126 assert_eq!(
6127 contacts(&client_d, cx_d),
6128 [("user_b".to_string(), "online", "busy")]
6129 );
6130
6131 server.forbid_connections();
6132 server.disconnect_client(client_a.peer_id().unwrap());
6133 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
6134 assert_eq!(contacts(&client_a, cx_a), []);
6135 assert_eq!(
6136 contacts(&client_b, cx_b),
6137 [
6138 ("user_a".to_string(), "offline", "free"),
6139 ("user_c".to_string(), "online", "free"),
6140 ("user_d".to_string(), "online", "free")
6141 ]
6142 );
6143 assert_eq!(
6144 contacts(&client_c, cx_c),
6145 [
6146 ("user_a".to_string(), "offline", "free"),
6147 ("user_b".to_string(), "online", "free")
6148 ]
6149 );
6150 assert_eq!(
6151 contacts(&client_d, cx_d),
6152 [("user_b".to_string(), "online", "free")]
6153 );
6154
6155 // Test removing a contact
6156 client_b
6157 .user_store()
6158 .update(cx_b, |store, cx| {
6159 store.remove_contact(client_c.user_id().unwrap(), cx)
6160 })
6161 .await
6162 .unwrap();
6163 executor.run_until_parked();
6164 assert_eq!(
6165 contacts(&client_b, cx_b),
6166 [
6167 ("user_a".to_string(), "offline", "free"),
6168 ("user_d".to_string(), "online", "free")
6169 ]
6170 );
6171 assert_eq!(
6172 contacts(&client_c, cx_c),
6173 [("user_a".to_string(), "offline", "free"),]
6174 );
6175
6176 fn contacts(
6177 client: &TestClient,
6178 cx: &TestAppContext,
6179 ) -> Vec<(String, &'static str, &'static str)> {
6180 client.user_store().read_with(cx, |store, _| {
6181 store
6182 .contacts()
6183 .iter()
6184 .map(|contact| {
6185 (
6186 contact.user.github_login.clone().to_string(),
6187 if contact.online { "online" } else { "offline" },
6188 if contact.busy { "busy" } else { "free" },
6189 )
6190 })
6191 .collect()
6192 })
6193 }
6194}
6195
6196#[gpui::test(iterations = 10)]
6197async fn test_contact_requests(
6198 executor: BackgroundExecutor,
6199 cx_a: &mut TestAppContext,
6200 cx_a2: &mut TestAppContext,
6201 cx_b: &mut TestAppContext,
6202 cx_b2: &mut TestAppContext,
6203 cx_c: &mut TestAppContext,
6204 cx_c2: &mut TestAppContext,
6205) {
6206 // Connect to a server as 3 clients.
6207 let mut server = TestServer::start(executor.clone()).await;
6208 let client_a = server.create_client(cx_a, "user_a").await;
6209 let client_a2 = server.create_client(cx_a2, "user_a").await;
6210 let client_b = server.create_client(cx_b, "user_b").await;
6211 let client_b2 = server.create_client(cx_b2, "user_b").await;
6212 let client_c = server.create_client(cx_c, "user_c").await;
6213 let client_c2 = server.create_client(cx_c2, "user_c").await;
6214
6215 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
6216 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
6217 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
6218
6219 // User A and User C request that user B become their contact.
6220 client_a
6221 .user_store()
6222 .update(cx_a, |store, cx| {
6223 store.request_contact(client_b.user_id().unwrap(), cx)
6224 })
6225 .await
6226 .unwrap();
6227 client_c
6228 .user_store()
6229 .update(cx_c, |store, cx| {
6230 store.request_contact(client_b.user_id().unwrap(), cx)
6231 })
6232 .await
6233 .unwrap();
6234 executor.run_until_parked();
6235
6236 // All users see the pending request appear in all their clients.
6237 assert_eq!(
6238 client_a.summarize_contacts(cx_a).outgoing_requests,
6239 &["user_b"]
6240 );
6241 assert_eq!(
6242 client_a2.summarize_contacts(cx_a2).outgoing_requests,
6243 &["user_b"]
6244 );
6245 assert_eq!(
6246 client_b.summarize_contacts(cx_b).incoming_requests,
6247 &["user_a", "user_c"]
6248 );
6249 assert_eq!(
6250 client_b2.summarize_contacts(cx_b2).incoming_requests,
6251 &["user_a", "user_c"]
6252 );
6253 assert_eq!(
6254 client_c.summarize_contacts(cx_c).outgoing_requests,
6255 &["user_b"]
6256 );
6257 assert_eq!(
6258 client_c2.summarize_contacts(cx_c2).outgoing_requests,
6259 &["user_b"]
6260 );
6261
6262 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
6263 disconnect_and_reconnect(&client_a, cx_a).await;
6264 disconnect_and_reconnect(&client_b, cx_b).await;
6265 disconnect_and_reconnect(&client_c, cx_c).await;
6266 executor.run_until_parked();
6267 assert_eq!(
6268 client_a.summarize_contacts(cx_a).outgoing_requests,
6269 &["user_b"]
6270 );
6271 assert_eq!(
6272 client_b.summarize_contacts(cx_b).incoming_requests,
6273 &["user_a", "user_c"]
6274 );
6275 assert_eq!(
6276 client_c.summarize_contacts(cx_c).outgoing_requests,
6277 &["user_b"]
6278 );
6279
6280 // User B accepts the request from user A.
6281 client_b
6282 .user_store()
6283 .update(cx_b, |store, cx| {
6284 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
6285 })
6286 .await
6287 .unwrap();
6288
6289 executor.run_until_parked();
6290
6291 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
6292 let contacts_b = client_b.summarize_contacts(cx_b);
6293 assert_eq!(contacts_b.current, &["user_a"]);
6294 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
6295 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6296 assert_eq!(contacts_b2.current, &["user_a"]);
6297 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
6298
6299 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
6300 let contacts_a = client_a.summarize_contacts(cx_a);
6301 assert_eq!(contacts_a.current, &["user_b"]);
6302 assert!(contacts_a.outgoing_requests.is_empty());
6303 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
6304 assert_eq!(contacts_a2.current, &["user_b"]);
6305 assert!(contacts_a2.outgoing_requests.is_empty());
6306
6307 // Contacts are present upon connecting (tested here via disconnect/reconnect)
6308 disconnect_and_reconnect(&client_a, cx_a).await;
6309 disconnect_and_reconnect(&client_b, cx_b).await;
6310 disconnect_and_reconnect(&client_c, cx_c).await;
6311 executor.run_until_parked();
6312 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6313 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6314 assert_eq!(
6315 client_b.summarize_contacts(cx_b).incoming_requests,
6316 &["user_c"]
6317 );
6318 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6319 assert_eq!(
6320 client_c.summarize_contacts(cx_c).outgoing_requests,
6321 &["user_b"]
6322 );
6323
6324 // User B rejects the request from user C.
6325 client_b
6326 .user_store()
6327 .update(cx_b, |store, cx| {
6328 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
6329 })
6330 .await
6331 .unwrap();
6332
6333 executor.run_until_parked();
6334
6335 // User B doesn't see user C as their contact, and the incoming request from them is removed.
6336 let contacts_b = client_b.summarize_contacts(cx_b);
6337 assert_eq!(contacts_b.current, &["user_a"]);
6338 assert!(contacts_b.incoming_requests.is_empty());
6339 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6340 assert_eq!(contacts_b2.current, &["user_a"]);
6341 assert!(contacts_b2.incoming_requests.is_empty());
6342
6343 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
6344 let contacts_c = client_c.summarize_contacts(cx_c);
6345 assert!(contacts_c.current.is_empty());
6346 assert!(contacts_c.outgoing_requests.is_empty());
6347 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
6348 assert!(contacts_c2.current.is_empty());
6349 assert!(contacts_c2.outgoing_requests.is_empty());
6350
6351 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
6352 disconnect_and_reconnect(&client_a, cx_a).await;
6353 disconnect_and_reconnect(&client_b, cx_b).await;
6354 disconnect_and_reconnect(&client_c, cx_c).await;
6355 executor.run_until_parked();
6356 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6357 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6358 assert!(
6359 client_b
6360 .summarize_contacts(cx_b)
6361 .incoming_requests
6362 .is_empty()
6363 );
6364 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6365 assert!(
6366 client_c
6367 .summarize_contacts(cx_c)
6368 .outgoing_requests
6369 .is_empty()
6370 );
6371
6372 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
6373 client.disconnect(&cx.to_async());
6374 client.clear_contacts(cx).await;
6375 client
6376 .connect(false, &cx.to_async())
6377 .await
6378 .into_response()
6379 .unwrap();
6380 }
6381}
6382
6383#[gpui::test(iterations = 10)]
6384async fn test_join_call_after_screen_was_shared(
6385 executor: BackgroundExecutor,
6386 cx_a: &mut TestAppContext,
6387 cx_b: &mut TestAppContext,
6388) {
6389 let mut server = TestServer::start(executor.clone()).await;
6390
6391 let client_a = server.create_client(cx_a, "user_a").await;
6392 let client_b = server.create_client(cx_b, "user_b").await;
6393 server
6394 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6395 .await;
6396
6397 let active_call_a = cx_a.read(ActiveCall::global);
6398 let active_call_b = cx_b.read(ActiveCall::global);
6399
6400 // Call users B and C from client A.
6401 active_call_a
6402 .update(cx_a, |call, cx| {
6403 call.invite(client_b.user_id().unwrap(), None, cx)
6404 })
6405 .await
6406 .unwrap();
6407
6408 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
6409 executor.run_until_parked();
6410 assert_eq!(
6411 room_participants(&room_a, cx_a),
6412 RoomParticipants {
6413 remote: Default::default(),
6414 pending: vec!["user_b".to_string()]
6415 }
6416 );
6417
6418 // User B receives the call.
6419
6420 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
6421 let call_b = incoming_call_b.next().await.unwrap().unwrap();
6422 assert_eq!(call_b.calling_user.github_login, "user_a");
6423
6424 // User A shares their screen
6425 let display = gpui::TestScreenCaptureSource::new();
6426 cx_a.set_screen_capture_sources(vec![display]);
6427 let screen_a = cx_a
6428 .update(|cx| cx.screen_capture_sources())
6429 .await
6430 .unwrap()
6431 .unwrap()
6432 .into_iter()
6433 .next()
6434 .unwrap();
6435
6436 active_call_a
6437 .update(cx_a, |call, cx| {
6438 call.room()
6439 .unwrap()
6440 .update(cx, |room, cx| room.share_screen(screen_a, cx))
6441 })
6442 .await
6443 .unwrap();
6444
6445 client_b.user_store().update(cx_b, |user_store, _| {
6446 user_store.clear_cache();
6447 });
6448
6449 // User B joins the room
6450 active_call_b
6451 .update(cx_b, |call, cx| call.accept_incoming(cx))
6452 .await
6453 .unwrap();
6454
6455 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
6456 assert!(incoming_call_b.next().await.unwrap().is_none());
6457
6458 executor.run_until_parked();
6459 assert_eq!(
6460 room_participants(&room_a, cx_a),
6461 RoomParticipants {
6462 remote: vec!["user_b".to_string()],
6463 pending: vec![],
6464 }
6465 );
6466 assert_eq!(
6467 room_participants(&room_b, cx_b),
6468 RoomParticipants {
6469 remote: vec!["user_a".to_string()],
6470 pending: vec![],
6471 }
6472 );
6473
6474 // Ensure User B sees User A's screenshare.
6475
6476 room_b.read_with(cx_b, |room, _| {
6477 assert_eq!(
6478 room.remote_participants()
6479 .get(&client_a.user_id().unwrap())
6480 .unwrap()
6481 .video_tracks
6482 .len(),
6483 1
6484 );
6485 });
6486}
6487
6488#[gpui::test]
6489async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
6490 let mut server = TestServer::start(cx.executor().clone()).await;
6491 let client_a = server.create_client(cx, "user_a").await;
6492 let (_workspace_a, cx) = client_a.build_test_workspace(cx).await;
6493
6494 cx.simulate_resize(size(px(300.), px(300.)));
6495
6496 cx.simulate_keystrokes("cmd-n cmd-n cmd-n");
6497 cx.update(|window, _cx| window.refresh());
6498
6499 let tab_bounds = cx.debug_bounds("TAB-2").unwrap();
6500 let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap();
6501
6502 assert!(
6503 tab_bounds.intersects(&new_tab_button_bounds),
6504 "Tab should overlap with the new tab button, if this is failing check if there's been a redesign!"
6505 );
6506
6507 cx.simulate_event(MouseDownEvent {
6508 button: MouseButton::Right,
6509 position: new_tab_button_bounds.center(),
6510 modifiers: Modifiers::default(),
6511 click_count: 1,
6512 first_mouse: false,
6513 });
6514
6515 // regression test that the right click menu for tabs does not open.
6516 assert!(cx.debug_bounds("MENU_ITEM-Close").is_none());
6517
6518 let tab_bounds = cx.debug_bounds("TAB-1").unwrap();
6519 cx.simulate_event(MouseDownEvent {
6520 button: MouseButton::Right,
6521 position: tab_bounds.center(),
6522 modifiers: Modifiers::default(),
6523 click_count: 1,
6524 first_mouse: false,
6525 });
6526 assert!(cx.debug_bounds("MENU_ITEM-Close").is_some());
6527}
6528
6529#[gpui::test]
6530async fn test_pane_split_left(cx: &mut TestAppContext) {
6531 let (_, client) = TestServer::start1(cx).await;
6532 let (workspace, cx) = client.build_test_workspace(cx).await;
6533
6534 cx.simulate_keystrokes("cmd-n");
6535 workspace.update(cx, |workspace, cx| {
6536 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 1);
6537 });
6538 cx.simulate_keystrokes("cmd-k left");
6539 workspace.update(cx, |workspace, cx| {
6540 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
6541 });
6542 cx.simulate_keystrokes("cmd-k");
6543 // sleep for longer than the timeout in keyboard shortcut handling
6544 // to verify that it doesn't fire in this case.
6545 cx.executor().advance_clock(Duration::from_secs(2));
6546 cx.simulate_keystrokes("left");
6547 workspace.update(cx, |workspace, cx| {
6548 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
6549 });
6550}
6551
6552#[gpui::test]
6553async fn test_join_after_restart(cx1: &mut TestAppContext, cx2: &mut TestAppContext) {
6554 let (mut server, client) = TestServer::start1(cx1).await;
6555 let channel1 = server.make_public_channel("channel1", &client, cx1).await;
6556 let channel2 = server.make_public_channel("channel2", &client, cx1).await;
6557
6558 join_channel(channel1, &client, cx1).await.unwrap();
6559 drop(client);
6560
6561 let client2 = server.create_client(cx2, "user_a").await;
6562 join_channel(channel2, &client2, cx2).await.unwrap();
6563}
6564
6565#[gpui::test]
6566async fn test_preview_tabs(cx: &mut TestAppContext) {
6567 let (_server, client) = TestServer::start1(cx).await;
6568 let (workspace, cx) = client.build_test_workspace(cx).await;
6569 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone());
6570
6571 let worktree_id = project.update(cx, |project, cx| {
6572 project.worktrees(cx).next().unwrap().read(cx).id()
6573 });
6574
6575 let path_1 = ProjectPath {
6576 worktree_id,
6577 path: Path::new("1.txt").into(),
6578 };
6579 let path_2 = ProjectPath {
6580 worktree_id,
6581 path: Path::new("2.js").into(),
6582 };
6583 let path_3 = ProjectPath {
6584 worktree_id,
6585 path: Path::new("3.rs").into(),
6586 };
6587
6588 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6589
6590 let get_path = |pane: &Pane, idx: usize, cx: &App| {
6591 pane.item_for_index(idx).unwrap().project_path(cx).unwrap()
6592 };
6593
6594 // Opening item 3 as a "permanent" tab
6595 workspace
6596 .update_in(cx, |workspace, window, cx| {
6597 workspace.open_path(path_3.clone(), None, false, window, cx)
6598 })
6599 .await
6600 .unwrap();
6601
6602 pane.update(cx, |pane, cx| {
6603 assert_eq!(pane.items_len(), 1);
6604 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6605 assert_eq!(pane.preview_item_id(), None);
6606
6607 assert!(!pane.can_navigate_backward());
6608 assert!(!pane.can_navigate_forward());
6609 });
6610
6611 // Open item 1 as preview
6612 workspace
6613 .update_in(cx, |workspace, window, cx| {
6614 workspace.open_path_preview(path_1.clone(), None, true, true, true, window, cx)
6615 })
6616 .await
6617 .unwrap();
6618
6619 pane.update(cx, |pane, cx| {
6620 assert_eq!(pane.items_len(), 2);
6621 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6622 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6623 assert_eq!(
6624 pane.preview_item_id(),
6625 Some(pane.items().nth(1).unwrap().item_id())
6626 );
6627
6628 assert!(pane.can_navigate_backward());
6629 assert!(!pane.can_navigate_forward());
6630 });
6631
6632 // Open item 2 as preview
6633 workspace
6634 .update_in(cx, |workspace, window, cx| {
6635 workspace.open_path_preview(path_2.clone(), None, true, true, true, window, cx)
6636 })
6637 .await
6638 .unwrap();
6639
6640 pane.update(cx, |pane, cx| {
6641 assert_eq!(pane.items_len(), 2);
6642 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6643 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6644 assert_eq!(
6645 pane.preview_item_id(),
6646 Some(pane.items().nth(1).unwrap().item_id())
6647 );
6648
6649 assert!(pane.can_navigate_backward());
6650 assert!(!pane.can_navigate_forward());
6651 });
6652
6653 // Going back should show item 1 as preview
6654 workspace
6655 .update_in(cx, |workspace, window, cx| {
6656 workspace.go_back(pane.downgrade(), window, cx)
6657 })
6658 .await
6659 .unwrap();
6660
6661 pane.update(cx, |pane, cx| {
6662 assert_eq!(pane.items_len(), 2);
6663 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6664 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6665 assert_eq!(
6666 pane.preview_item_id(),
6667 Some(pane.items().nth(1).unwrap().item_id())
6668 );
6669
6670 assert!(pane.can_navigate_backward());
6671 assert!(pane.can_navigate_forward());
6672 });
6673
6674 // Closing item 1
6675 pane.update_in(cx, |pane, window, cx| {
6676 pane.close_item_by_id(
6677 pane.active_item().unwrap().item_id(),
6678 workspace::SaveIntent::Skip,
6679 window,
6680 cx,
6681 )
6682 })
6683 .await
6684 .unwrap();
6685
6686 pane.update(cx, |pane, cx| {
6687 assert_eq!(pane.items_len(), 1);
6688 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6689 assert_eq!(pane.preview_item_id(), None);
6690
6691 assert!(pane.can_navigate_backward());
6692 assert!(!pane.can_navigate_forward());
6693 });
6694
6695 // Going back should show item 1 as preview
6696 workspace
6697 .update_in(cx, |workspace, window, cx| {
6698 workspace.go_back(pane.downgrade(), window, cx)
6699 })
6700 .await
6701 .unwrap();
6702
6703 pane.update(cx, |pane, cx| {
6704 assert_eq!(pane.items_len(), 2);
6705 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6706 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6707 assert_eq!(
6708 pane.preview_item_id(),
6709 Some(pane.items().nth(1).unwrap().item_id())
6710 );
6711
6712 assert!(pane.can_navigate_backward());
6713 assert!(pane.can_navigate_forward());
6714 });
6715
6716 // Close permanent tab
6717 pane.update_in(cx, |pane, window, cx| {
6718 let id = pane.items().next().unwrap().item_id();
6719 pane.close_item_by_id(id, workspace::SaveIntent::Skip, window, cx)
6720 })
6721 .await
6722 .unwrap();
6723
6724 pane.update(cx, |pane, cx| {
6725 assert_eq!(pane.items_len(), 1);
6726 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6727 assert_eq!(
6728 pane.preview_item_id(),
6729 Some(pane.items().next().unwrap().item_id())
6730 );
6731
6732 assert!(pane.can_navigate_backward());
6733 assert!(pane.can_navigate_forward());
6734 });
6735
6736 // Split pane to the right
6737 pane.update(cx, |pane, cx| {
6738 pane.split(workspace::SplitDirection::Right, cx);
6739 });
6740
6741 let right_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6742
6743 pane.update(cx, |pane, cx| {
6744 assert_eq!(pane.items_len(), 1);
6745 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6746 assert_eq!(
6747 pane.preview_item_id(),
6748 Some(pane.items().next().unwrap().item_id())
6749 );
6750
6751 assert!(pane.can_navigate_backward());
6752 assert!(pane.can_navigate_forward());
6753 });
6754
6755 right_pane.update(cx, |pane, cx| {
6756 assert_eq!(pane.items_len(), 1);
6757 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6758 assert_eq!(pane.preview_item_id(), None);
6759
6760 assert!(!pane.can_navigate_backward());
6761 assert!(!pane.can_navigate_forward());
6762 });
6763
6764 // Open item 2 as preview in right pane
6765 workspace
6766 .update_in(cx, |workspace, window, cx| {
6767 workspace.open_path_preview(path_2.clone(), None, true, true, true, window, cx)
6768 })
6769 .await
6770 .unwrap();
6771
6772 pane.update(cx, |pane, cx| {
6773 assert_eq!(pane.items_len(), 1);
6774 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6775 assert_eq!(
6776 pane.preview_item_id(),
6777 Some(pane.items().next().unwrap().item_id())
6778 );
6779
6780 assert!(pane.can_navigate_backward());
6781 assert!(pane.can_navigate_forward());
6782 });
6783
6784 right_pane.update(cx, |pane, cx| {
6785 assert_eq!(pane.items_len(), 2);
6786 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6787 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6788 assert_eq!(
6789 pane.preview_item_id(),
6790 Some(pane.items().nth(1).unwrap().item_id())
6791 );
6792
6793 assert!(pane.can_navigate_backward());
6794 assert!(!pane.can_navigate_forward());
6795 });
6796
6797 // Focus left pane
6798 workspace.update_in(cx, |workspace, window, cx| {
6799 workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx)
6800 });
6801
6802 // Open item 2 as preview in left pane
6803 workspace
6804 .update_in(cx, |workspace, window, cx| {
6805 workspace.open_path_preview(path_2.clone(), None, true, true, true, window, cx)
6806 })
6807 .await
6808 .unwrap();
6809
6810 pane.update(cx, |pane, cx| {
6811 assert_eq!(pane.items_len(), 1);
6812 assert_eq!(get_path(pane, 0, cx), path_2.clone());
6813 assert_eq!(
6814 pane.preview_item_id(),
6815 Some(pane.items().next().unwrap().item_id())
6816 );
6817
6818 assert!(pane.can_navigate_backward());
6819 assert!(!pane.can_navigate_forward());
6820 });
6821
6822 right_pane.update(cx, |pane, cx| {
6823 assert_eq!(pane.items_len(), 2);
6824 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6825 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6826 assert_eq!(
6827 pane.preview_item_id(),
6828 Some(pane.items().nth(1).unwrap().item_id())
6829 );
6830
6831 assert!(pane.can_navigate_backward());
6832 assert!(!pane.can_navigate_forward());
6833 });
6834}
6835
6836#[gpui::test(iterations = 10)]
6837async fn test_context_collaboration_with_reconnect(
6838 executor: BackgroundExecutor,
6839 cx_a: &mut TestAppContext,
6840 cx_b: &mut TestAppContext,
6841) {
6842 let mut server = TestServer::start(executor.clone()).await;
6843 let client_a = server.create_client(cx_a, "user_a").await;
6844 let client_b = server.create_client(cx_b, "user_b").await;
6845 server
6846 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6847 .await;
6848 let active_call_a = cx_a.read(ActiveCall::global);
6849
6850 client_a.fs().insert_tree("/a", Default::default()).await;
6851 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
6852 let project_id = active_call_a
6853 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6854 .await
6855 .unwrap();
6856 let project_b = client_b.join_remote_project(project_id, cx_b).await;
6857
6858 // Client A sees that a guest has joined.
6859 executor.run_until_parked();
6860
6861 project_a.read_with(cx_a, |project, _| {
6862 assert_eq!(project.collaborators().len(), 1);
6863 });
6864 project_b.read_with(cx_b, |project, _| {
6865 assert_eq!(project.collaborators().len(), 1);
6866 });
6867
6868 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
6869 let context_store_a = cx_a
6870 .update(|cx| {
6871 ContextStore::new(
6872 project_a.clone(),
6873 prompt_builder.clone(),
6874 Arc::new(SlashCommandWorkingSet::default()),
6875 cx,
6876 )
6877 })
6878 .await
6879 .unwrap();
6880 let context_store_b = cx_b
6881 .update(|cx| {
6882 ContextStore::new(
6883 project_b.clone(),
6884 prompt_builder.clone(),
6885 Arc::new(SlashCommandWorkingSet::default()),
6886 cx,
6887 )
6888 })
6889 .await
6890 .unwrap();
6891
6892 // Client A creates a new chats.
6893 let context_a = context_store_a.update(cx_a, |store, cx| store.create(cx));
6894 executor.run_until_parked();
6895
6896 // Client B retrieves host's contexts and joins one.
6897 let context_b = context_store_b
6898 .update(cx_b, |store, cx| {
6899 let host_contexts = store.host_contexts().to_vec();
6900 assert_eq!(host_contexts.len(), 1);
6901 store.open_remote_context(host_contexts[0].id.clone(), cx)
6902 })
6903 .await
6904 .unwrap();
6905
6906 // Host and guest make changes
6907 context_a.update(cx_a, |context, cx| {
6908 context.buffer().update(cx, |buffer, cx| {
6909 buffer.edit([(0..0, "Host change\n")], None, cx)
6910 })
6911 });
6912 context_b.update(cx_b, |context, cx| {
6913 context.buffer().update(cx, |buffer, cx| {
6914 buffer.edit([(0..0, "Guest change\n")], None, cx)
6915 })
6916 });
6917 executor.run_until_parked();
6918 assert_eq!(
6919 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6920 "Guest change\nHost change\n"
6921 );
6922 assert_eq!(
6923 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6924 "Guest change\nHost change\n"
6925 );
6926
6927 // Disconnect client A and make some changes while disconnected.
6928 server.disconnect_client(client_a.peer_id().unwrap());
6929 server.forbid_connections();
6930 context_a.update(cx_a, |context, cx| {
6931 context.buffer().update(cx, |buffer, cx| {
6932 buffer.edit([(0..0, "Host offline change\n")], None, cx)
6933 })
6934 });
6935 context_b.update(cx_b, |context, cx| {
6936 context.buffer().update(cx, |buffer, cx| {
6937 buffer.edit([(0..0, "Guest offline change\n")], None, cx)
6938 })
6939 });
6940 executor.run_until_parked();
6941 assert_eq!(
6942 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6943 "Host offline change\nGuest change\nHost change\n"
6944 );
6945 assert_eq!(
6946 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6947 "Guest offline change\nGuest change\nHost change\n"
6948 );
6949
6950 // Allow client A to reconnect and verify that contexts converge.
6951 server.allow_connections();
6952 executor.advance_clock(RECEIVE_TIMEOUT);
6953 assert_eq!(
6954 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6955 "Guest offline change\nHost offline change\nGuest change\nHost change\n"
6956 );
6957 assert_eq!(
6958 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6959 "Guest offline change\nHost offline change\nGuest change\nHost change\n"
6960 );
6961
6962 // Client A disconnects without being able to reconnect. Context B becomes readonly.
6963 server.forbid_connections();
6964 server.disconnect_client(client_a.peer_id().unwrap());
6965 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
6966 context_b.read_with(cx_b, |context, cx| {
6967 assert!(context.buffer().read(cx).read_only());
6968 });
6969}
6970
6971#[gpui::test]
6972async fn test_remote_git_branches(
6973 executor: BackgroundExecutor,
6974 cx_a: &mut TestAppContext,
6975 cx_b: &mut TestAppContext,
6976) {
6977 let mut server = TestServer::start(executor.clone()).await;
6978 let client_a = server.create_client(cx_a, "user_a").await;
6979 let client_b = server.create_client(cx_b, "user_b").await;
6980 server
6981 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6982 .await;
6983 let active_call_a = cx_a.read(ActiveCall::global);
6984
6985 client_a
6986 .fs()
6987 .insert_tree("/project", serde_json::json!({ ".git":{} }))
6988 .await;
6989 let branches = ["main", "dev", "feature-1"];
6990 client_a
6991 .fs()
6992 .insert_branches(Path::new("/project/.git"), &branches);
6993 let branches_set = branches
6994 .into_iter()
6995 .map(ToString::to_string)
6996 .collect::<HashSet<_>>();
6997
6998 let (project_a, _) = client_a.build_local_project("/project", cx_a).await;
6999
7000 let project_id = active_call_a
7001 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7002 .await
7003 .unwrap();
7004 let project_b = client_b.join_remote_project(project_id, cx_b).await;
7005
7006 // Client A sees that a guest has joined and the repo has been populated
7007 executor.run_until_parked();
7008
7009 let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
7010
7011 let branches_b = cx_b
7012 .update(|cx| repo_b.update(cx, |repository, _| repository.branches()))
7013 .await
7014 .unwrap()
7015 .unwrap();
7016
7017 let new_branch = branches[2];
7018
7019 let branches_b = branches_b
7020 .into_iter()
7021 .map(|branch| branch.name().to_string())
7022 .collect::<HashSet<_>>();
7023
7024 assert_eq!(branches_b, branches_set);
7025
7026 cx_b.update(|cx| {
7027 repo_b.update(cx, |repository, _cx| {
7028 repository.change_branch(new_branch.to_string())
7029 })
7030 })
7031 .await
7032 .unwrap()
7033 .unwrap();
7034
7035 executor.run_until_parked();
7036
7037 let host_branch = cx_a.update(|cx| {
7038 project_a.update(cx, |project, cx| {
7039 project
7040 .repositories(cx)
7041 .values()
7042 .next()
7043 .unwrap()
7044 .read(cx)
7045 .branch
7046 .as_ref()
7047 .unwrap()
7048 .clone()
7049 })
7050 });
7051
7052 assert_eq!(host_branch.name(), branches[2]);
7053
7054 // Also try creating a new branch
7055 cx_b.update(|cx| {
7056 repo_b.update(cx, |repository, _cx| {
7057 repository.create_branch("totally-new-branch".to_string())
7058 })
7059 })
7060 .await
7061 .unwrap()
7062 .unwrap();
7063
7064 cx_b.update(|cx| {
7065 repo_b.update(cx, |repository, _cx| {
7066 repository.change_branch("totally-new-branch".to_string())
7067 })
7068 })
7069 .await
7070 .unwrap()
7071 .unwrap();
7072
7073 executor.run_until_parked();
7074
7075 let host_branch = cx_a.update(|cx| {
7076 project_a.update(cx, |project, cx| {
7077 project
7078 .repositories(cx)
7079 .values()
7080 .next()
7081 .unwrap()
7082 .read(cx)
7083 .branch
7084 .as_ref()
7085 .unwrap()
7086 .clone()
7087 })
7088 });
7089
7090 assert_eq!(host_branch.name(), "totally-new-branch");
7091}