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