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