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