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