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::lsp_store::FormatTarget;
31use project::{
32 lsp_store::FormatTrigger, search::SearchQuery, search::SearchResult, DiagnosticSummary,
33 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 }
4210 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4211 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4212 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
4213 lsp::WorkDoneProgressEnd { message: None },
4214 )),
4215 });
4216
4217 // When the "disk base diagnostics finished" message is received, the buffers'
4218 // diagnostics are expected to be present.
4219 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
4220 project_b.update(cx_b, {
4221 let project_b = project_b.clone();
4222 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
4223 move |_, cx| {
4224 cx.subscribe(&project_b, move |_, _, event, cx| {
4225 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
4226 disk_based_diagnostics_finished.store(true, SeqCst);
4227 for (buffer, _) in &guest_buffers {
4228 assert_eq!(
4229 buffer
4230 .read(cx)
4231 .snapshot()
4232 .diagnostics_in_range::<_, usize>(0..5, false)
4233 .count(),
4234 1,
4235 "expected a diagnostic for buffer {:?}",
4236 buffer.read(cx).file().unwrap().path(),
4237 );
4238 }
4239 }
4240 })
4241 .detach();
4242 }
4243 });
4244
4245 executor.run_until_parked();
4246 assert!(disk_based_diagnostics_finished.load(SeqCst));
4247}
4248
4249#[gpui::test(iterations = 10)]
4250async fn test_reloading_buffer_manually(
4251 executor: BackgroundExecutor,
4252 cx_a: &mut TestAppContext,
4253 cx_b: &mut TestAppContext,
4254) {
4255 let mut server = TestServer::start(executor.clone()).await;
4256 let client_a = server.create_client(cx_a, "user_a").await;
4257 let client_b = server.create_client(cx_b, "user_b").await;
4258 server
4259 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4260 .await;
4261 let active_call_a = cx_a.read(ActiveCall::global);
4262
4263 client_a
4264 .fs()
4265 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
4266 .await;
4267 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4268 let buffer_a = project_a
4269 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4270 .await
4271 .unwrap();
4272 let project_id = active_call_a
4273 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4274 .await
4275 .unwrap();
4276
4277 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4278
4279 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4280 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4281 buffer_b.update(cx_b, |buffer, cx| {
4282 buffer.edit([(4..7, "six")], None, cx);
4283 buffer.edit([(10..11, "6")], None, cx);
4284 assert_eq!(buffer.text(), "let six = 6;");
4285 assert!(buffer.is_dirty());
4286 assert!(!buffer.has_conflict());
4287 });
4288 executor.run_until_parked();
4289
4290 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4291
4292 client_a
4293 .fs()
4294 .save(
4295 "/a/a.rs".as_ref(),
4296 &Rope::from("let seven = 7;"),
4297 LineEnding::Unix,
4298 )
4299 .await
4300 .unwrap();
4301 executor.run_until_parked();
4302
4303 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4304
4305 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4306
4307 project_b
4308 .update(cx_b, |project, cx| {
4309 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4310 })
4311 .await
4312 .unwrap();
4313
4314 buffer_a.read_with(cx_a, |buffer, _| {
4315 assert_eq!(buffer.text(), "let seven = 7;");
4316 assert!(!buffer.is_dirty());
4317 assert!(!buffer.has_conflict());
4318 });
4319
4320 buffer_b.read_with(cx_b, |buffer, _| {
4321 assert_eq!(buffer.text(), "let seven = 7;");
4322 assert!(!buffer.is_dirty());
4323 assert!(!buffer.has_conflict());
4324 });
4325
4326 buffer_a.update(cx_a, |buffer, cx| {
4327 // Undoing on the host is a no-op when the reload was initiated by the guest.
4328 buffer.undo(cx);
4329 assert_eq!(buffer.text(), "let seven = 7;");
4330 assert!(!buffer.is_dirty());
4331 assert!(!buffer.has_conflict());
4332 });
4333 buffer_b.update(cx_b, |buffer, cx| {
4334 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4335 buffer.undo(cx);
4336 assert_eq!(buffer.text(), "let six = 6;");
4337 assert!(buffer.is_dirty());
4338 assert!(!buffer.has_conflict());
4339 });
4340}
4341
4342#[gpui::test(iterations = 10)]
4343async fn test_formatting_buffer(
4344 executor: BackgroundExecutor,
4345 cx_a: &mut TestAppContext,
4346 cx_b: &mut TestAppContext,
4347) {
4348 let mut server = TestServer::start(executor.clone()).await;
4349 let client_a = server.create_client(cx_a, "user_a").await;
4350 let client_b = server.create_client(cx_b, "user_b").await;
4351 server
4352 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4353 .await;
4354 let active_call_a = cx_a.read(ActiveCall::global);
4355
4356 client_a.language_registry().add(rust_lang());
4357 let mut fake_language_servers = client_a
4358 .language_registry()
4359 .register_fake_lsp("Rust", FakeLspAdapter::default());
4360
4361 // Here we insert a fake tree with a directory that exists on disk. This is needed
4362 // because later we'll invoke a command, which requires passing a working directory
4363 // that points to a valid location on disk.
4364 let directory = env::current_dir().unwrap();
4365 client_a
4366 .fs()
4367 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4368 .await;
4369 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4370 let project_id = active_call_a
4371 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4372 .await
4373 .unwrap();
4374 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4375 let lsp_store_b = project_b.update(cx_b, |p, _| p.lsp_store());
4376
4377 let buffer_b = project_b
4378 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4379 .await
4380 .unwrap();
4381
4382 let _handle = lsp_store_b.update(cx_b, |lsp_store, cx| {
4383 lsp_store.register_buffer_with_language_servers(&buffer_b, cx)
4384 });
4385 let fake_language_server = fake_language_servers.next().await.unwrap();
4386 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4387 Ok(Some(vec![
4388 lsp::TextEdit {
4389 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4390 new_text: "h".to_string(),
4391 },
4392 lsp::TextEdit {
4393 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4394 new_text: "y".to_string(),
4395 },
4396 ]))
4397 });
4398
4399 project_b
4400 .update(cx_b, |project, cx| {
4401 project.format(
4402 HashSet::from_iter([buffer_b.clone()]),
4403 true,
4404 FormatTrigger::Save,
4405 FormatTarget::Buffer,
4406 cx,
4407 )
4408 })
4409 .await
4410 .unwrap();
4411
4412 // The edits from the LSP are applied, and a final newline is added.
4413 assert_eq!(
4414 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4415 "let honey = \"two\"\n"
4416 );
4417
4418 // Ensure buffer can be formatted using an external command. Notice how the
4419 // host's configuration is honored as opposed to using the guest's settings.
4420 cx_a.update(|cx| {
4421 SettingsStore::update_global(cx, |store, cx| {
4422 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4423 file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
4424 vec![Formatter::External {
4425 command: "awk".into(),
4426 arguments: Some(vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into()),
4427 }]
4428 .into(),
4429 )));
4430 });
4431 });
4432 });
4433
4434 executor.allow_parking();
4435 project_b
4436 .update(cx_b, |project, cx| {
4437 project.format(
4438 HashSet::from_iter([buffer_b.clone()]),
4439 true,
4440 FormatTrigger::Save,
4441 FormatTarget::Buffer,
4442 cx,
4443 )
4444 })
4445 .await
4446 .unwrap();
4447 assert_eq!(
4448 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4449 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4450 );
4451}
4452
4453#[gpui::test(iterations = 10)]
4454async fn test_prettier_formatting_buffer(
4455 executor: BackgroundExecutor,
4456 cx_a: &mut TestAppContext,
4457 cx_b: &mut TestAppContext,
4458) {
4459 let mut server = TestServer::start(executor.clone()).await;
4460 let client_a = server.create_client(cx_a, "user_a").await;
4461 let client_b = server.create_client(cx_b, "user_b").await;
4462 server
4463 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4464 .await;
4465 let active_call_a = cx_a.read(ActiveCall::global);
4466
4467 let test_plugin = "test_plugin";
4468
4469 client_a.language_registry().add(Arc::new(Language::new(
4470 LanguageConfig {
4471 name: "TypeScript".into(),
4472 matcher: LanguageMatcher {
4473 path_suffixes: vec!["ts".to_string()],
4474 ..Default::default()
4475 },
4476 ..Default::default()
4477 },
4478 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
4479 )));
4480 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4481 "TypeScript",
4482 FakeLspAdapter {
4483 prettier_plugins: vec![test_plugin],
4484 ..Default::default()
4485 },
4486 );
4487
4488 // Here we insert a fake tree with a directory that exists on disk. This is needed
4489 // because later we'll invoke a command, which requires passing a working directory
4490 // that points to a valid location on disk.
4491 let directory = env::current_dir().unwrap();
4492 let buffer_text = "let one = \"two\"";
4493 client_a
4494 .fs()
4495 .insert_tree(&directory, json!({ "a.ts": buffer_text }))
4496 .await;
4497 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4498 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
4499 let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
4500 let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
4501
4502 let project_id = active_call_a
4503 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4504 .await
4505 .unwrap();
4506 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4507 let (buffer_b, _) = project_b
4508 .update(cx_b, |p, cx| {
4509 p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
4510 })
4511 .await
4512 .unwrap();
4513
4514 cx_a.update(|cx| {
4515 SettingsStore::update_global(cx, |store, cx| {
4516 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4517 file.defaults.formatter = Some(SelectedFormatter::Auto);
4518 file.defaults.prettier = Some(PrettierSettings {
4519 allowed: true,
4520 ..PrettierSettings::default()
4521 });
4522 });
4523 });
4524 });
4525 cx_b.update(|cx| {
4526 SettingsStore::update_global(cx, |store, cx| {
4527 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4528 file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
4529 vec![Formatter::LanguageServer { name: None }].into(),
4530 )));
4531 file.defaults.prettier = Some(PrettierSettings {
4532 allowed: true,
4533 ..PrettierSettings::default()
4534 });
4535 });
4536 });
4537 });
4538 let fake_language_server = fake_language_servers.next().await.unwrap();
4539 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4540 panic!(
4541 "Unexpected: prettier should be preferred since it's enabled and language supports it"
4542 )
4543 });
4544
4545 project_b
4546 .update(cx_b, |project, cx| {
4547 project.format(
4548 HashSet::from_iter([buffer_b.clone()]),
4549 true,
4550 FormatTrigger::Save,
4551 FormatTarget::Buffer,
4552 cx,
4553 )
4554 })
4555 .await
4556 .unwrap();
4557
4558 executor.run_until_parked();
4559 assert_eq!(
4560 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4561 buffer_text.to_string() + "\n" + prettier_format_suffix,
4562 "Prettier formatting was not applied to client buffer after client's request"
4563 );
4564
4565 project_a
4566 .update(cx_a, |project, cx| {
4567 project.format(
4568 HashSet::from_iter([buffer_a.clone()]),
4569 true,
4570 FormatTrigger::Manual,
4571 FormatTarget::Buffer,
4572 cx,
4573 )
4574 })
4575 .await
4576 .unwrap();
4577
4578 executor.run_until_parked();
4579 assert_eq!(
4580 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4581 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
4582 "Prettier formatting was not applied to client buffer after host's request"
4583 );
4584}
4585
4586#[gpui::test(iterations = 10)]
4587async fn test_definition(
4588 executor: BackgroundExecutor,
4589 cx_a: &mut TestAppContext,
4590 cx_b: &mut TestAppContext,
4591) {
4592 let mut server = TestServer::start(executor.clone()).await;
4593 let client_a = server.create_client(cx_a, "user_a").await;
4594 let client_b = server.create_client(cx_b, "user_b").await;
4595 server
4596 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4597 .await;
4598 let active_call_a = cx_a.read(ActiveCall::global);
4599
4600 let mut fake_language_servers = client_a
4601 .language_registry()
4602 .register_fake_lsp("Rust", Default::default());
4603 client_a.language_registry().add(rust_lang());
4604
4605 client_a
4606 .fs()
4607 .insert_tree(
4608 "/root",
4609 json!({
4610 "dir-1": {
4611 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4612 },
4613 "dir-2": {
4614 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4615 "c.rs": "type T2 = usize;",
4616 }
4617 }),
4618 )
4619 .await;
4620 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4621 let project_id = active_call_a
4622 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4623 .await
4624 .unwrap();
4625 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4626
4627 // Open the file on client B.
4628 let (buffer_b, _handle) = project_b
4629 .update(cx_b, |p, cx| {
4630 p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
4631 })
4632 .await
4633 .unwrap();
4634
4635 // Request the definition of a symbol as the guest.
4636 let fake_language_server = fake_language_servers.next().await.unwrap();
4637 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4638 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4639 lsp::Location::new(
4640 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4641 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4642 ),
4643 )))
4644 });
4645
4646 let definitions_1 = project_b
4647 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
4648 .await
4649 .unwrap();
4650 cx_b.read(|cx| {
4651 assert_eq!(definitions_1.len(), 1);
4652 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4653 let target_buffer = definitions_1[0].target.buffer.read(cx);
4654 assert_eq!(
4655 target_buffer.text(),
4656 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4657 );
4658 assert_eq!(
4659 definitions_1[0].target.range.to_point(target_buffer),
4660 Point::new(0, 6)..Point::new(0, 9)
4661 );
4662 });
4663
4664 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4665 // the previous call to `definition`.
4666 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4667 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4668 lsp::Location::new(
4669 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4670 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4671 ),
4672 )))
4673 });
4674
4675 let definitions_2 = project_b
4676 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4677 .await
4678 .unwrap();
4679 cx_b.read(|cx| {
4680 assert_eq!(definitions_2.len(), 1);
4681 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4682 let target_buffer = definitions_2[0].target.buffer.read(cx);
4683 assert_eq!(
4684 target_buffer.text(),
4685 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4686 );
4687 assert_eq!(
4688 definitions_2[0].target.range.to_point(target_buffer),
4689 Point::new(1, 6)..Point::new(1, 11)
4690 );
4691 });
4692 assert_eq!(
4693 definitions_1[0].target.buffer,
4694 definitions_2[0].target.buffer
4695 );
4696
4697 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4698 |req, _| async move {
4699 assert_eq!(
4700 req.text_document_position_params.position,
4701 lsp::Position::new(0, 7)
4702 );
4703 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4704 lsp::Location::new(
4705 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4706 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4707 ),
4708 )))
4709 },
4710 );
4711
4712 let type_definitions = project_b
4713 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4714 .await
4715 .unwrap();
4716 cx_b.read(|cx| {
4717 assert_eq!(type_definitions.len(), 1);
4718 let target_buffer = type_definitions[0].target.buffer.read(cx);
4719 assert_eq!(target_buffer.text(), "type T2 = usize;");
4720 assert_eq!(
4721 type_definitions[0].target.range.to_point(target_buffer),
4722 Point::new(0, 5)..Point::new(0, 7)
4723 );
4724 });
4725}
4726
4727#[gpui::test(iterations = 10)]
4728async fn test_references(
4729 executor: BackgroundExecutor,
4730 cx_a: &mut TestAppContext,
4731 cx_b: &mut TestAppContext,
4732) {
4733 let mut server = TestServer::start(executor.clone()).await;
4734 let client_a = server.create_client(cx_a, "user_a").await;
4735 let client_b = server.create_client(cx_b, "user_b").await;
4736 server
4737 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4738 .await;
4739 let active_call_a = cx_a.read(ActiveCall::global);
4740
4741 client_a.language_registry().add(rust_lang());
4742 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4743 "Rust",
4744 FakeLspAdapter {
4745 name: "my-fake-lsp-adapter",
4746 capabilities: lsp::ServerCapabilities {
4747 references_provider: Some(lsp::OneOf::Left(true)),
4748 ..Default::default()
4749 },
4750 ..Default::default()
4751 },
4752 );
4753
4754 client_a
4755 .fs()
4756 .insert_tree(
4757 "/root",
4758 json!({
4759 "dir-1": {
4760 "one.rs": "const ONE: usize = 1;",
4761 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4762 },
4763 "dir-2": {
4764 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4765 }
4766 }),
4767 )
4768 .await;
4769 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4770 let project_id = active_call_a
4771 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4772 .await
4773 .unwrap();
4774 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4775
4776 // Open the file on client B.
4777 let (buffer_b, _handle) = project_b
4778 .update(cx_b, |p, cx| {
4779 p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
4780 })
4781 .await
4782 .unwrap();
4783
4784 // Request references to a symbol as the guest.
4785 let fake_language_server = fake_language_servers.next().await.unwrap();
4786 let (lsp_response_tx, rx) = mpsc::unbounded::<Result<Option<Vec<lsp::Location>>>>();
4787 fake_language_server.handle_request::<lsp::request::References, _, _>({
4788 let rx = Arc::new(Mutex::new(Some(rx)));
4789 move |params, _| {
4790 assert_eq!(
4791 params.text_document_position.text_document.uri.as_str(),
4792 "file:///root/dir-1/one.rs"
4793 );
4794 let rx = rx.clone();
4795 async move {
4796 let mut response_rx = rx.lock().take().unwrap();
4797 let result = response_rx.next().await.unwrap();
4798 *rx.lock() = Some(response_rx);
4799 result
4800 }
4801 }
4802 });
4803
4804 let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx));
4805
4806 // User is informed that a request is pending.
4807 executor.run_until_parked();
4808 project_b.read_with(cx_b, |project, cx| {
4809 let status = project.language_server_statuses(cx).next().unwrap().1;
4810 assert_eq!(status.name, "my-fake-lsp-adapter");
4811 assert_eq!(
4812 status.pending_work.values().next().unwrap().message,
4813 Some("Finding references...".into())
4814 );
4815 });
4816
4817 // Cause the language server to respond.
4818 lsp_response_tx
4819 .unbounded_send(Ok(Some(vec![
4820 lsp::Location {
4821 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4822 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4823 },
4824 lsp::Location {
4825 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4826 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4827 },
4828 lsp::Location {
4829 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4830 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4831 },
4832 ])))
4833 .unwrap();
4834
4835 let references = references.await.unwrap();
4836 executor.run_until_parked();
4837 project_b.read_with(cx_b, |project, cx| {
4838 // User is informed that a request is no longer pending.
4839 let status = project.language_server_statuses(cx).next().unwrap().1;
4840 assert!(status.pending_work.is_empty());
4841
4842 assert_eq!(references.len(), 3);
4843 assert_eq!(project.worktrees(cx).count(), 2);
4844
4845 let two_buffer = references[0].buffer.read(cx);
4846 let three_buffer = references[2].buffer.read(cx);
4847 assert_eq!(
4848 two_buffer.file().unwrap().path().as_ref(),
4849 Path::new("two.rs")
4850 );
4851 assert_eq!(references[1].buffer, references[0].buffer);
4852 assert_eq!(
4853 three_buffer.file().unwrap().full_path(cx),
4854 Path::new("/root/dir-2/three.rs")
4855 );
4856
4857 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4858 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4859 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4860 });
4861
4862 let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx));
4863
4864 // User is informed that a request is pending.
4865 executor.run_until_parked();
4866 project_b.read_with(cx_b, |project, cx| {
4867 let status = project.language_server_statuses(cx).next().unwrap().1;
4868 assert_eq!(status.name, "my-fake-lsp-adapter");
4869 assert_eq!(
4870 status.pending_work.values().next().unwrap().message,
4871 Some("Finding references...".into())
4872 );
4873 });
4874
4875 // Cause the LSP request to fail.
4876 lsp_response_tx
4877 .unbounded_send(Err(anyhow!("can't find references")))
4878 .unwrap();
4879 references.await.unwrap_err();
4880
4881 // User is informed that the request is no longer pending.
4882 executor.run_until_parked();
4883 project_b.read_with(cx_b, |project, cx| {
4884 let status = project.language_server_statuses(cx).next().unwrap().1;
4885 assert!(status.pending_work.is_empty());
4886 });
4887}
4888
4889#[gpui::test(iterations = 10)]
4890async fn test_project_search(
4891 executor: BackgroundExecutor,
4892 cx_a: &mut TestAppContext,
4893 cx_b: &mut TestAppContext,
4894) {
4895 let mut server = TestServer::start(executor.clone()).await;
4896 let client_a = server.create_client(cx_a, "user_a").await;
4897 let client_b = server.create_client(cx_b, "user_b").await;
4898 server
4899 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4900 .await;
4901 let active_call_a = cx_a.read(ActiveCall::global);
4902
4903 client_a
4904 .fs()
4905 .insert_tree(
4906 "/root",
4907 json!({
4908 "dir-1": {
4909 "a": "hello world",
4910 "b": "goodnight moon",
4911 "c": "a world of goo",
4912 "d": "world champion of clown world",
4913 },
4914 "dir-2": {
4915 "e": "disney world is fun",
4916 }
4917 }),
4918 )
4919 .await;
4920 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4921 let (worktree_2, _) = project_a
4922 .update(cx_a, |p, cx| {
4923 p.find_or_create_worktree("/root/dir-2", true, cx)
4924 })
4925 .await
4926 .unwrap();
4927 worktree_2
4928 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4929 .await;
4930 let project_id = active_call_a
4931 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4932 .await
4933 .unwrap();
4934
4935 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4936
4937 // Perform a search as the guest.
4938 let mut results = HashMap::default();
4939 let mut search_rx = project_b.update(cx_b, |project, cx| {
4940 project.search(
4941 SearchQuery::text(
4942 "world",
4943 false,
4944 false,
4945 false,
4946 Default::default(),
4947 Default::default(),
4948 None,
4949 )
4950 .unwrap(),
4951 cx,
4952 )
4953 });
4954 while let Some(result) = search_rx.next().await {
4955 match result {
4956 SearchResult::Buffer { buffer, ranges } => {
4957 results.entry(buffer).or_insert(ranges);
4958 }
4959 SearchResult::LimitReached => {
4960 panic!("Unexpectedly reached search limit in tests. If you do want to assert limit-reached, change this panic call.")
4961 }
4962 };
4963 }
4964
4965 let mut ranges_by_path = results
4966 .into_iter()
4967 .map(|(buffer, ranges)| {
4968 buffer.read_with(cx_b, |buffer, cx| {
4969 let path = buffer.file().unwrap().full_path(cx);
4970 let offset_ranges = ranges
4971 .into_iter()
4972 .map(|range| range.to_offset(buffer))
4973 .collect::<Vec<_>>();
4974 (path, offset_ranges)
4975 })
4976 })
4977 .collect::<Vec<_>>();
4978 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4979
4980 assert_eq!(
4981 ranges_by_path,
4982 &[
4983 (PathBuf::from("dir-1/a"), vec![6..11]),
4984 (PathBuf::from("dir-1/c"), vec![2..7]),
4985 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4986 (PathBuf::from("dir-2/e"), vec![7..12]),
4987 ]
4988 );
4989}
4990
4991#[gpui::test(iterations = 10)]
4992async fn test_document_highlights(
4993 executor: BackgroundExecutor,
4994 cx_a: &mut TestAppContext,
4995 cx_b: &mut TestAppContext,
4996) {
4997 let mut server = TestServer::start(executor.clone()).await;
4998 let client_a = server.create_client(cx_a, "user_a").await;
4999 let client_b = server.create_client(cx_b, "user_b").await;
5000 server
5001 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5002 .await;
5003 let active_call_a = cx_a.read(ActiveCall::global);
5004
5005 client_a
5006 .fs()
5007 .insert_tree(
5008 "/root-1",
5009 json!({
5010 "main.rs": "fn double(number: i32) -> i32 { number + number }",
5011 }),
5012 )
5013 .await;
5014
5015 let mut fake_language_servers = client_a
5016 .language_registry()
5017 .register_fake_lsp("Rust", Default::default());
5018 client_a.language_registry().add(rust_lang());
5019
5020 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
5021 let project_id = active_call_a
5022 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5023 .await
5024 .unwrap();
5025 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5026
5027 // Open the file on client B.
5028 let (buffer_b, _handle) = project_b
5029 .update(cx_b, |p, cx| {
5030 p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
5031 })
5032 .await
5033 .unwrap();
5034
5035 // Request document highlights as the guest.
5036 let fake_language_server = fake_language_servers.next().await.unwrap();
5037 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
5038 |params, _| async move {
5039 assert_eq!(
5040 params
5041 .text_document_position_params
5042 .text_document
5043 .uri
5044 .as_str(),
5045 "file:///root-1/main.rs"
5046 );
5047 assert_eq!(
5048 params.text_document_position_params.position,
5049 lsp::Position::new(0, 34)
5050 );
5051 Ok(Some(vec![
5052 lsp::DocumentHighlight {
5053 kind: Some(lsp::DocumentHighlightKind::WRITE),
5054 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
5055 },
5056 lsp::DocumentHighlight {
5057 kind: Some(lsp::DocumentHighlightKind::READ),
5058 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
5059 },
5060 lsp::DocumentHighlight {
5061 kind: Some(lsp::DocumentHighlightKind::READ),
5062 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
5063 },
5064 ]))
5065 },
5066 );
5067
5068 let highlights = project_b
5069 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
5070 .await
5071 .unwrap();
5072
5073 buffer_b.read_with(cx_b, |buffer, _| {
5074 let snapshot = buffer.snapshot();
5075
5076 let highlights = highlights
5077 .into_iter()
5078 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
5079 .collect::<Vec<_>>();
5080 assert_eq!(
5081 highlights,
5082 &[
5083 (lsp::DocumentHighlightKind::WRITE, 10..16),
5084 (lsp::DocumentHighlightKind::READ, 32..38),
5085 (lsp::DocumentHighlightKind::READ, 41..47)
5086 ]
5087 )
5088 });
5089}
5090
5091#[gpui::test(iterations = 10)]
5092async fn test_lsp_hover(
5093 executor: BackgroundExecutor,
5094 cx_a: &mut TestAppContext,
5095 cx_b: &mut TestAppContext,
5096) {
5097 let mut server = TestServer::start(executor.clone()).await;
5098 let client_a = server.create_client(cx_a, "user_a").await;
5099 let client_b = server.create_client(cx_b, "user_b").await;
5100 server
5101 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5102 .await;
5103 let active_call_a = cx_a.read(ActiveCall::global);
5104
5105 client_a
5106 .fs()
5107 .insert_tree(
5108 "/root-1",
5109 json!({
5110 "main.rs": "use std::collections::HashMap;",
5111 }),
5112 )
5113 .await;
5114
5115 client_a.language_registry().add(rust_lang());
5116 let language_server_names = ["rust-analyzer", "CrabLang-ls"];
5117 let mut language_servers = [
5118 client_a.language_registry().register_fake_lsp(
5119 "Rust",
5120 FakeLspAdapter {
5121 name: "rust-analyzer",
5122 capabilities: lsp::ServerCapabilities {
5123 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5124 ..lsp::ServerCapabilities::default()
5125 },
5126 ..FakeLspAdapter::default()
5127 },
5128 ),
5129 client_a.language_registry().register_fake_lsp(
5130 "Rust",
5131 FakeLspAdapter {
5132 name: "CrabLang-ls",
5133 capabilities: lsp::ServerCapabilities {
5134 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5135 ..lsp::ServerCapabilities::default()
5136 },
5137 ..FakeLspAdapter::default()
5138 },
5139 ),
5140 ];
5141
5142 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
5143 let project_id = active_call_a
5144 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5145 .await
5146 .unwrap();
5147 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5148
5149 // Open the file as the guest
5150 let (buffer_b, _handle) = project_b
5151 .update(cx_b, |p, cx| {
5152 p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
5153 })
5154 .await
5155 .unwrap();
5156
5157 let mut servers_with_hover_requests = HashMap::default();
5158 for i in 0..language_server_names.len() {
5159 let new_server = language_servers[i].next().await.unwrap_or_else(|| {
5160 panic!(
5161 "Failed to get language server #{i} with name {}",
5162 &language_server_names[i]
5163 )
5164 });
5165 let new_server_name = new_server.server.name();
5166 assert!(
5167 !servers_with_hover_requests.contains_key(&new_server_name),
5168 "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
5169 );
5170 match new_server_name.as_ref() {
5171 "CrabLang-ls" => {
5172 servers_with_hover_requests.insert(
5173 new_server_name.clone(),
5174 new_server.handle_request::<lsp::request::HoverRequest, _, _>(
5175 move |params, _| {
5176 assert_eq!(
5177 params
5178 .text_document_position_params
5179 .text_document
5180 .uri
5181 .as_str(),
5182 "file:///root-1/main.rs"
5183 );
5184 let name = new_server_name.clone();
5185 async move {
5186 Ok(Some(lsp::Hover {
5187 contents: lsp::HoverContents::Scalar(
5188 lsp::MarkedString::String(format!("{name} hover")),
5189 ),
5190 range: None,
5191 }))
5192 }
5193 },
5194 ),
5195 );
5196 }
5197 "rust-analyzer" => {
5198 servers_with_hover_requests.insert(
5199 new_server_name.clone(),
5200 new_server.handle_request::<lsp::request::HoverRequest, _, _>(
5201 |params, _| async move {
5202 assert_eq!(
5203 params
5204 .text_document_position_params
5205 .text_document
5206 .uri
5207 .as_str(),
5208 "file:///root-1/main.rs"
5209 );
5210 assert_eq!(
5211 params.text_document_position_params.position,
5212 lsp::Position::new(0, 22)
5213 );
5214 Ok(Some(lsp::Hover {
5215 contents: lsp::HoverContents::Array(vec![
5216 lsp::MarkedString::String("Test hover content.".to_string()),
5217 lsp::MarkedString::LanguageString(lsp::LanguageString {
5218 language: "Rust".to_string(),
5219 value: "let foo = 42;".to_string(),
5220 }),
5221 ]),
5222 range: Some(lsp::Range::new(
5223 lsp::Position::new(0, 22),
5224 lsp::Position::new(0, 29),
5225 )),
5226 }))
5227 },
5228 ),
5229 );
5230 }
5231 unexpected => panic!("Unexpected server name: {unexpected}"),
5232 }
5233 }
5234
5235 // Request hover information as the guest.
5236 let mut hovers = project_b
5237 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
5238 .await;
5239 assert_eq!(
5240 hovers.len(),
5241 2,
5242 "Expected two hovers from both language servers, but got: {hovers:?}"
5243 );
5244
5245 let _: Vec<()> = futures::future::join_all(servers_with_hover_requests.into_values().map(
5246 |mut hover_request| async move {
5247 hover_request
5248 .next()
5249 .await
5250 .expect("All hover requests should have been triggered")
5251 },
5252 ))
5253 .await;
5254
5255 hovers.sort_by_key(|hover| hover.contents.len());
5256 let first_hover = hovers.first().cloned().unwrap();
5257 assert_eq!(
5258 first_hover.contents,
5259 vec![project::HoverBlock {
5260 text: "CrabLang-ls hover".to_string(),
5261 kind: HoverBlockKind::Markdown,
5262 },]
5263 );
5264 let second_hover = hovers.last().cloned().unwrap();
5265 assert_eq!(
5266 second_hover.contents,
5267 vec![
5268 project::HoverBlock {
5269 text: "Test hover content.".to_string(),
5270 kind: HoverBlockKind::Markdown,
5271 },
5272 project::HoverBlock {
5273 text: "let foo = 42;".to_string(),
5274 kind: HoverBlockKind::Code {
5275 language: "Rust".to_string()
5276 },
5277 }
5278 ]
5279 );
5280 buffer_b.read_with(cx_b, |buffer, _| {
5281 let snapshot = buffer.snapshot();
5282 assert_eq!(second_hover.range.unwrap().to_offset(&snapshot), 22..29);
5283 });
5284}
5285
5286#[gpui::test(iterations = 10)]
5287async fn test_project_symbols(
5288 executor: BackgroundExecutor,
5289 cx_a: &mut TestAppContext,
5290 cx_b: &mut TestAppContext,
5291) {
5292 let mut server = TestServer::start(executor.clone()).await;
5293 let client_a = server.create_client(cx_a, "user_a").await;
5294 let client_b = server.create_client(cx_b, "user_b").await;
5295 server
5296 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5297 .await;
5298 let active_call_a = cx_a.read(ActiveCall::global);
5299
5300 client_a.language_registry().add(rust_lang());
5301 let mut fake_language_servers = client_a
5302 .language_registry()
5303 .register_fake_lsp("Rust", Default::default());
5304
5305 client_a
5306 .fs()
5307 .insert_tree(
5308 "/code",
5309 json!({
5310 "crate-1": {
5311 "one.rs": "const ONE: usize = 1;",
5312 },
5313 "crate-2": {
5314 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
5315 },
5316 "private": {
5317 "passwords.txt": "the-password",
5318 }
5319 }),
5320 )
5321 .await;
5322 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
5323 let project_id = active_call_a
5324 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5325 .await
5326 .unwrap();
5327 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5328
5329 // Cause the language server to start.
5330 let _buffer = project_b
5331 .update(cx_b, |p, cx| {
5332 p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
5333 })
5334 .await
5335 .unwrap();
5336
5337 let fake_language_server = fake_language_servers.next().await.unwrap();
5338 fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
5339 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
5340 #[allow(deprecated)]
5341 lsp::SymbolInformation {
5342 name: "TWO".into(),
5343 location: lsp::Location {
5344 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
5345 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5346 },
5347 kind: lsp::SymbolKind::CONSTANT,
5348 tags: None,
5349 container_name: None,
5350 deprecated: None,
5351 },
5352 ])))
5353 });
5354
5355 // Request the definition of a symbol as the guest.
5356 let symbols = project_b
5357 .update(cx_b, |p, cx| p.symbols("two", cx))
5358 .await
5359 .unwrap();
5360 assert_eq!(symbols.len(), 1);
5361 assert_eq!(symbols[0].name, "TWO");
5362
5363 // Open one of the returned symbols.
5364 let buffer_b_2 = project_b
5365 .update(cx_b, |project, cx| {
5366 project.open_buffer_for_symbol(&symbols[0], cx)
5367 })
5368 .await
5369 .unwrap();
5370
5371 buffer_b_2.read_with(cx_b, |buffer, cx| {
5372 assert_eq!(
5373 buffer.file().unwrap().full_path(cx),
5374 Path::new("/code/crate-2/two.rs")
5375 );
5376 });
5377
5378 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
5379 let mut fake_symbol = symbols[0].clone();
5380 fake_symbol.path.path = Path::new("/code/secrets").into();
5381 let error = project_b
5382 .update(cx_b, |project, cx| {
5383 project.open_buffer_for_symbol(&fake_symbol, cx)
5384 })
5385 .await
5386 .unwrap_err();
5387 assert!(error.to_string().contains("invalid symbol signature"));
5388}
5389
5390#[gpui::test(iterations = 10)]
5391async fn test_open_buffer_while_getting_definition_pointing_to_it(
5392 executor: BackgroundExecutor,
5393 cx_a: &mut TestAppContext,
5394 cx_b: &mut TestAppContext,
5395 mut rng: StdRng,
5396) {
5397 let mut server = TestServer::start(executor.clone()).await;
5398 let client_a = server.create_client(cx_a, "user_a").await;
5399 let client_b = server.create_client(cx_b, "user_b").await;
5400 server
5401 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5402 .await;
5403 let active_call_a = cx_a.read(ActiveCall::global);
5404
5405 client_a.language_registry().add(rust_lang());
5406 let mut fake_language_servers = client_a
5407 .language_registry()
5408 .register_fake_lsp("Rust", Default::default());
5409
5410 client_a
5411 .fs()
5412 .insert_tree(
5413 "/root",
5414 json!({
5415 "a.rs": "const ONE: usize = b::TWO;",
5416 "b.rs": "const TWO: usize = 2",
5417 }),
5418 )
5419 .await;
5420 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
5421 let project_id = active_call_a
5422 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5423 .await
5424 .unwrap();
5425 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5426
5427 let (buffer_b1, _lsp) = project_b
5428 .update(cx_b, |p, cx| {
5429 p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
5430 })
5431 .await
5432 .unwrap();
5433
5434 let fake_language_server = fake_language_servers.next().await.unwrap();
5435 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
5436 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
5437 lsp::Location::new(
5438 lsp::Url::from_file_path("/root/b.rs").unwrap(),
5439 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5440 ),
5441 )))
5442 });
5443
5444 let definitions;
5445 let buffer_b2;
5446 if rng.gen() {
5447 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5448 (buffer_b2, _) = project_b
5449 .update(cx_b, |p, cx| {
5450 p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
5451 })
5452 .await
5453 .unwrap();
5454 } else {
5455 (buffer_b2, _) = project_b
5456 .update(cx_b, |p, cx| {
5457 p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
5458 })
5459 .await
5460 .unwrap();
5461 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5462 }
5463
5464 let definitions = definitions.await.unwrap();
5465 assert_eq!(definitions.len(), 1);
5466 assert_eq!(definitions[0].target.buffer, buffer_b2);
5467}
5468
5469#[gpui::test(iterations = 10)]
5470async fn test_contacts(
5471 executor: BackgroundExecutor,
5472 cx_a: &mut TestAppContext,
5473 cx_b: &mut TestAppContext,
5474 cx_c: &mut TestAppContext,
5475 cx_d: &mut TestAppContext,
5476) {
5477 let mut server = TestServer::start(executor.clone()).await;
5478 let client_a = server.create_client(cx_a, "user_a").await;
5479 let client_b = server.create_client(cx_b, "user_b").await;
5480 let client_c = server.create_client(cx_c, "user_c").await;
5481 let client_d = server.create_client(cx_d, "user_d").await;
5482 server
5483 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5484 .await;
5485 let active_call_a = cx_a.read(ActiveCall::global);
5486 let active_call_b = cx_b.read(ActiveCall::global);
5487 let active_call_c = cx_c.read(ActiveCall::global);
5488 let _active_call_d = cx_d.read(ActiveCall::global);
5489
5490 executor.run_until_parked();
5491 assert_eq!(
5492 contacts(&client_a, cx_a),
5493 [
5494 ("user_b".to_string(), "online", "free"),
5495 ("user_c".to_string(), "online", "free")
5496 ]
5497 );
5498 assert_eq!(
5499 contacts(&client_b, cx_b),
5500 [
5501 ("user_a".to_string(), "online", "free"),
5502 ("user_c".to_string(), "online", "free")
5503 ]
5504 );
5505 assert_eq!(
5506 contacts(&client_c, cx_c),
5507 [
5508 ("user_a".to_string(), "online", "free"),
5509 ("user_b".to_string(), "online", "free")
5510 ]
5511 );
5512 assert_eq!(contacts(&client_d, cx_d), []);
5513
5514 server.disconnect_client(client_c.peer_id().unwrap());
5515 server.forbid_connections();
5516 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5517 assert_eq!(
5518 contacts(&client_a, cx_a),
5519 [
5520 ("user_b".to_string(), "online", "free"),
5521 ("user_c".to_string(), "offline", "free")
5522 ]
5523 );
5524 assert_eq!(
5525 contacts(&client_b, cx_b),
5526 [
5527 ("user_a".to_string(), "online", "free"),
5528 ("user_c".to_string(), "offline", "free")
5529 ]
5530 );
5531 assert_eq!(contacts(&client_c, cx_c), []);
5532 assert_eq!(contacts(&client_d, cx_d), []);
5533
5534 server.allow_connections();
5535 client_c
5536 .authenticate_and_connect(false, &cx_c.to_async())
5537 .await
5538 .unwrap();
5539
5540 executor.run_until_parked();
5541 assert_eq!(
5542 contacts(&client_a, cx_a),
5543 [
5544 ("user_b".to_string(), "online", "free"),
5545 ("user_c".to_string(), "online", "free")
5546 ]
5547 );
5548 assert_eq!(
5549 contacts(&client_b, cx_b),
5550 [
5551 ("user_a".to_string(), "online", "free"),
5552 ("user_c".to_string(), "online", "free")
5553 ]
5554 );
5555 assert_eq!(
5556 contacts(&client_c, cx_c),
5557 [
5558 ("user_a".to_string(), "online", "free"),
5559 ("user_b".to_string(), "online", "free")
5560 ]
5561 );
5562 assert_eq!(contacts(&client_d, cx_d), []);
5563
5564 active_call_a
5565 .update(cx_a, |call, cx| {
5566 call.invite(client_b.user_id().unwrap(), None, cx)
5567 })
5568 .await
5569 .unwrap();
5570 executor.run_until_parked();
5571 assert_eq!(
5572 contacts(&client_a, cx_a),
5573 [
5574 ("user_b".to_string(), "online", "busy"),
5575 ("user_c".to_string(), "online", "free")
5576 ]
5577 );
5578 assert_eq!(
5579 contacts(&client_b, cx_b),
5580 [
5581 ("user_a".to_string(), "online", "busy"),
5582 ("user_c".to_string(), "online", "free")
5583 ]
5584 );
5585 assert_eq!(
5586 contacts(&client_c, cx_c),
5587 [
5588 ("user_a".to_string(), "online", "busy"),
5589 ("user_b".to_string(), "online", "busy")
5590 ]
5591 );
5592 assert_eq!(contacts(&client_d, cx_d), []);
5593
5594 // Client B and client D become contacts while client B is being called.
5595 server
5596 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5597 .await;
5598 executor.run_until_parked();
5599 assert_eq!(
5600 contacts(&client_a, cx_a),
5601 [
5602 ("user_b".to_string(), "online", "busy"),
5603 ("user_c".to_string(), "online", "free")
5604 ]
5605 );
5606 assert_eq!(
5607 contacts(&client_b, cx_b),
5608 [
5609 ("user_a".to_string(), "online", "busy"),
5610 ("user_c".to_string(), "online", "free"),
5611 ("user_d".to_string(), "online", "free"),
5612 ]
5613 );
5614 assert_eq!(
5615 contacts(&client_c, cx_c),
5616 [
5617 ("user_a".to_string(), "online", "busy"),
5618 ("user_b".to_string(), "online", "busy")
5619 ]
5620 );
5621 assert_eq!(
5622 contacts(&client_d, cx_d),
5623 [("user_b".to_string(), "online", "busy")]
5624 );
5625
5626 active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
5627 executor.run_until_parked();
5628 assert_eq!(
5629 contacts(&client_a, cx_a),
5630 [
5631 ("user_b".to_string(), "online", "free"),
5632 ("user_c".to_string(), "online", "free")
5633 ]
5634 );
5635 assert_eq!(
5636 contacts(&client_b, cx_b),
5637 [
5638 ("user_a".to_string(), "online", "free"),
5639 ("user_c".to_string(), "online", "free"),
5640 ("user_d".to_string(), "online", "free")
5641 ]
5642 );
5643 assert_eq!(
5644 contacts(&client_c, cx_c),
5645 [
5646 ("user_a".to_string(), "online", "free"),
5647 ("user_b".to_string(), "online", "free")
5648 ]
5649 );
5650 assert_eq!(
5651 contacts(&client_d, cx_d),
5652 [("user_b".to_string(), "online", "free")]
5653 );
5654
5655 active_call_c
5656 .update(cx_c, |call, cx| {
5657 call.invite(client_a.user_id().unwrap(), None, cx)
5658 })
5659 .await
5660 .unwrap();
5661 executor.run_until_parked();
5662 assert_eq!(
5663 contacts(&client_a, cx_a),
5664 [
5665 ("user_b".to_string(), "online", "free"),
5666 ("user_c".to_string(), "online", "busy")
5667 ]
5668 );
5669 assert_eq!(
5670 contacts(&client_b, cx_b),
5671 [
5672 ("user_a".to_string(), "online", "busy"),
5673 ("user_c".to_string(), "online", "busy"),
5674 ("user_d".to_string(), "online", "free")
5675 ]
5676 );
5677 assert_eq!(
5678 contacts(&client_c, cx_c),
5679 [
5680 ("user_a".to_string(), "online", "busy"),
5681 ("user_b".to_string(), "online", "free")
5682 ]
5683 );
5684 assert_eq!(
5685 contacts(&client_d, cx_d),
5686 [("user_b".to_string(), "online", "free")]
5687 );
5688
5689 active_call_a
5690 .update(cx_a, |call, cx| call.accept_incoming(cx))
5691 .await
5692 .unwrap();
5693 executor.run_until_parked();
5694 assert_eq!(
5695 contacts(&client_a, cx_a),
5696 [
5697 ("user_b".to_string(), "online", "free"),
5698 ("user_c".to_string(), "online", "busy")
5699 ]
5700 );
5701 assert_eq!(
5702 contacts(&client_b, cx_b),
5703 [
5704 ("user_a".to_string(), "online", "busy"),
5705 ("user_c".to_string(), "online", "busy"),
5706 ("user_d".to_string(), "online", "free")
5707 ]
5708 );
5709 assert_eq!(
5710 contacts(&client_c, cx_c),
5711 [
5712 ("user_a".to_string(), "online", "busy"),
5713 ("user_b".to_string(), "online", "free")
5714 ]
5715 );
5716 assert_eq!(
5717 contacts(&client_d, cx_d),
5718 [("user_b".to_string(), "online", "free")]
5719 );
5720
5721 active_call_a
5722 .update(cx_a, |call, cx| {
5723 call.invite(client_b.user_id().unwrap(), None, cx)
5724 })
5725 .await
5726 .unwrap();
5727 executor.run_until_parked();
5728 assert_eq!(
5729 contacts(&client_a, cx_a),
5730 [
5731 ("user_b".to_string(), "online", "busy"),
5732 ("user_c".to_string(), "online", "busy")
5733 ]
5734 );
5735 assert_eq!(
5736 contacts(&client_b, cx_b),
5737 [
5738 ("user_a".to_string(), "online", "busy"),
5739 ("user_c".to_string(), "online", "busy"),
5740 ("user_d".to_string(), "online", "free")
5741 ]
5742 );
5743 assert_eq!(
5744 contacts(&client_c, cx_c),
5745 [
5746 ("user_a".to_string(), "online", "busy"),
5747 ("user_b".to_string(), "online", "busy")
5748 ]
5749 );
5750 assert_eq!(
5751 contacts(&client_d, cx_d),
5752 [("user_b".to_string(), "online", "busy")]
5753 );
5754
5755 active_call_a
5756 .update(cx_a, |call, cx| call.hang_up(cx))
5757 .await
5758 .unwrap();
5759 executor.run_until_parked();
5760 assert_eq!(
5761 contacts(&client_a, cx_a),
5762 [
5763 ("user_b".to_string(), "online", "free"),
5764 ("user_c".to_string(), "online", "free")
5765 ]
5766 );
5767 assert_eq!(
5768 contacts(&client_b, cx_b),
5769 [
5770 ("user_a".to_string(), "online", "free"),
5771 ("user_c".to_string(), "online", "free"),
5772 ("user_d".to_string(), "online", "free")
5773 ]
5774 );
5775 assert_eq!(
5776 contacts(&client_c, cx_c),
5777 [
5778 ("user_a".to_string(), "online", "free"),
5779 ("user_b".to_string(), "online", "free")
5780 ]
5781 );
5782 assert_eq!(
5783 contacts(&client_d, cx_d),
5784 [("user_b".to_string(), "online", "free")]
5785 );
5786
5787 active_call_a
5788 .update(cx_a, |call, cx| {
5789 call.invite(client_b.user_id().unwrap(), None, cx)
5790 })
5791 .await
5792 .unwrap();
5793 executor.run_until_parked();
5794 assert_eq!(
5795 contacts(&client_a, cx_a),
5796 [
5797 ("user_b".to_string(), "online", "busy"),
5798 ("user_c".to_string(), "online", "free")
5799 ]
5800 );
5801 assert_eq!(
5802 contacts(&client_b, cx_b),
5803 [
5804 ("user_a".to_string(), "online", "busy"),
5805 ("user_c".to_string(), "online", "free"),
5806 ("user_d".to_string(), "online", "free")
5807 ]
5808 );
5809 assert_eq!(
5810 contacts(&client_c, cx_c),
5811 [
5812 ("user_a".to_string(), "online", "busy"),
5813 ("user_b".to_string(), "online", "busy")
5814 ]
5815 );
5816 assert_eq!(
5817 contacts(&client_d, cx_d),
5818 [("user_b".to_string(), "online", "busy")]
5819 );
5820
5821 server.forbid_connections();
5822 server.disconnect_client(client_a.peer_id().unwrap());
5823 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5824 assert_eq!(contacts(&client_a, cx_a), []);
5825 assert_eq!(
5826 contacts(&client_b, cx_b),
5827 [
5828 ("user_a".to_string(), "offline", "free"),
5829 ("user_c".to_string(), "online", "free"),
5830 ("user_d".to_string(), "online", "free")
5831 ]
5832 );
5833 assert_eq!(
5834 contacts(&client_c, cx_c),
5835 [
5836 ("user_a".to_string(), "offline", "free"),
5837 ("user_b".to_string(), "online", "free")
5838 ]
5839 );
5840 assert_eq!(
5841 contacts(&client_d, cx_d),
5842 [("user_b".to_string(), "online", "free")]
5843 );
5844
5845 // Test removing a contact
5846 client_b
5847 .user_store()
5848 .update(cx_b, |store, cx| {
5849 store.remove_contact(client_c.user_id().unwrap(), cx)
5850 })
5851 .await
5852 .unwrap();
5853 executor.run_until_parked();
5854 assert_eq!(
5855 contacts(&client_b, cx_b),
5856 [
5857 ("user_a".to_string(), "offline", "free"),
5858 ("user_d".to_string(), "online", "free")
5859 ]
5860 );
5861 assert_eq!(
5862 contacts(&client_c, cx_c),
5863 [("user_a".to_string(), "offline", "free"),]
5864 );
5865
5866 fn contacts(
5867 client: &TestClient,
5868 cx: &TestAppContext,
5869 ) -> Vec<(String, &'static str, &'static str)> {
5870 client.user_store().read_with(cx, |store, _| {
5871 store
5872 .contacts()
5873 .iter()
5874 .map(|contact| {
5875 (
5876 contact.user.github_login.clone(),
5877 if contact.online { "online" } else { "offline" },
5878 if contact.busy { "busy" } else { "free" },
5879 )
5880 })
5881 .collect()
5882 })
5883 }
5884}
5885
5886#[gpui::test(iterations = 10)]
5887async fn test_contact_requests(
5888 executor: BackgroundExecutor,
5889 cx_a: &mut TestAppContext,
5890 cx_a2: &mut TestAppContext,
5891 cx_b: &mut TestAppContext,
5892 cx_b2: &mut TestAppContext,
5893 cx_c: &mut TestAppContext,
5894 cx_c2: &mut TestAppContext,
5895) {
5896 // Connect to a server as 3 clients.
5897 let mut server = TestServer::start(executor.clone()).await;
5898 let client_a = server.create_client(cx_a, "user_a").await;
5899 let client_a2 = server.create_client(cx_a2, "user_a").await;
5900 let client_b = server.create_client(cx_b, "user_b").await;
5901 let client_b2 = server.create_client(cx_b2, "user_b").await;
5902 let client_c = server.create_client(cx_c, "user_c").await;
5903 let client_c2 = server.create_client(cx_c2, "user_c").await;
5904
5905 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
5906 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
5907 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
5908
5909 // User A and User C request that user B become their contact.
5910 client_a
5911 .user_store()
5912 .update(cx_a, |store, cx| {
5913 store.request_contact(client_b.user_id().unwrap(), cx)
5914 })
5915 .await
5916 .unwrap();
5917 client_c
5918 .user_store()
5919 .update(cx_c, |store, cx| {
5920 store.request_contact(client_b.user_id().unwrap(), cx)
5921 })
5922 .await
5923 .unwrap();
5924 executor.run_until_parked();
5925
5926 // All users see the pending request appear in all their clients.
5927 assert_eq!(
5928 client_a.summarize_contacts(cx_a).outgoing_requests,
5929 &["user_b"]
5930 );
5931 assert_eq!(
5932 client_a2.summarize_contacts(cx_a2).outgoing_requests,
5933 &["user_b"]
5934 );
5935 assert_eq!(
5936 client_b.summarize_contacts(cx_b).incoming_requests,
5937 &["user_a", "user_c"]
5938 );
5939 assert_eq!(
5940 client_b2.summarize_contacts(cx_b2).incoming_requests,
5941 &["user_a", "user_c"]
5942 );
5943 assert_eq!(
5944 client_c.summarize_contacts(cx_c).outgoing_requests,
5945 &["user_b"]
5946 );
5947 assert_eq!(
5948 client_c2.summarize_contacts(cx_c2).outgoing_requests,
5949 &["user_b"]
5950 );
5951
5952 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
5953 disconnect_and_reconnect(&client_a, cx_a).await;
5954 disconnect_and_reconnect(&client_b, cx_b).await;
5955 disconnect_and_reconnect(&client_c, cx_c).await;
5956 executor.run_until_parked();
5957 assert_eq!(
5958 client_a.summarize_contacts(cx_a).outgoing_requests,
5959 &["user_b"]
5960 );
5961 assert_eq!(
5962 client_b.summarize_contacts(cx_b).incoming_requests,
5963 &["user_a", "user_c"]
5964 );
5965 assert_eq!(
5966 client_c.summarize_contacts(cx_c).outgoing_requests,
5967 &["user_b"]
5968 );
5969
5970 // User B accepts the request from user A.
5971 client_b
5972 .user_store()
5973 .update(cx_b, |store, cx| {
5974 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5975 })
5976 .await
5977 .unwrap();
5978
5979 executor.run_until_parked();
5980
5981 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
5982 let contacts_b = client_b.summarize_contacts(cx_b);
5983 assert_eq!(contacts_b.current, &["user_a"]);
5984 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
5985 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5986 assert_eq!(contacts_b2.current, &["user_a"]);
5987 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
5988
5989 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
5990 let contacts_a = client_a.summarize_contacts(cx_a);
5991 assert_eq!(contacts_a.current, &["user_b"]);
5992 assert!(contacts_a.outgoing_requests.is_empty());
5993 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
5994 assert_eq!(contacts_a2.current, &["user_b"]);
5995 assert!(contacts_a2.outgoing_requests.is_empty());
5996
5997 // Contacts are present upon connecting (tested here via disconnect/reconnect)
5998 disconnect_and_reconnect(&client_a, cx_a).await;
5999 disconnect_and_reconnect(&client_b, cx_b).await;
6000 disconnect_and_reconnect(&client_c, cx_c).await;
6001 executor.run_until_parked();
6002 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6003 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6004 assert_eq!(
6005 client_b.summarize_contacts(cx_b).incoming_requests,
6006 &["user_c"]
6007 );
6008 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6009 assert_eq!(
6010 client_c.summarize_contacts(cx_c).outgoing_requests,
6011 &["user_b"]
6012 );
6013
6014 // User B rejects the request from user C.
6015 client_b
6016 .user_store()
6017 .update(cx_b, |store, cx| {
6018 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
6019 })
6020 .await
6021 .unwrap();
6022
6023 executor.run_until_parked();
6024
6025 // User B doesn't see user C as their contact, and the incoming request from them is removed.
6026 let contacts_b = client_b.summarize_contacts(cx_b);
6027 assert_eq!(contacts_b.current, &["user_a"]);
6028 assert!(contacts_b.incoming_requests.is_empty());
6029 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6030 assert_eq!(contacts_b2.current, &["user_a"]);
6031 assert!(contacts_b2.incoming_requests.is_empty());
6032
6033 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
6034 let contacts_c = client_c.summarize_contacts(cx_c);
6035 assert!(contacts_c.current.is_empty());
6036 assert!(contacts_c.outgoing_requests.is_empty());
6037 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
6038 assert!(contacts_c2.current.is_empty());
6039 assert!(contacts_c2.outgoing_requests.is_empty());
6040
6041 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
6042 disconnect_and_reconnect(&client_a, cx_a).await;
6043 disconnect_and_reconnect(&client_b, cx_b).await;
6044 disconnect_and_reconnect(&client_c, cx_c).await;
6045 executor.run_until_parked();
6046 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6047 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6048 assert!(client_b
6049 .summarize_contacts(cx_b)
6050 .incoming_requests
6051 .is_empty());
6052 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6053 assert!(client_c
6054 .summarize_contacts(cx_c)
6055 .outgoing_requests
6056 .is_empty());
6057
6058 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
6059 client.disconnect(&cx.to_async());
6060 client.clear_contacts(cx).await;
6061 client
6062 .authenticate_and_connect(false, &cx.to_async())
6063 .await
6064 .unwrap();
6065 }
6066}
6067
6068// TODO: Re-enable this test once we can replace our swift Livekit SDK with the rust SDK
6069#[cfg(not(target_os = "macos"))]
6070#[gpui::test(iterations = 10)]
6071async fn test_join_call_after_screen_was_shared(
6072 executor: BackgroundExecutor,
6073 cx_a: &mut TestAppContext,
6074 cx_b: &mut TestAppContext,
6075) {
6076 let mut server = TestServer::start(executor.clone()).await;
6077
6078 let client_a = server.create_client(cx_a, "user_a").await;
6079 let client_b = server.create_client(cx_b, "user_b").await;
6080 server
6081 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6082 .await;
6083
6084 let active_call_a = cx_a.read(ActiveCall::global);
6085 let active_call_b = cx_b.read(ActiveCall::global);
6086
6087 // Call users B and C from client A.
6088 active_call_a
6089 .update(cx_a, |call, cx| {
6090 call.invite(client_b.user_id().unwrap(), None, cx)
6091 })
6092 .await
6093 .unwrap();
6094
6095 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
6096 executor.run_until_parked();
6097 assert_eq!(
6098 room_participants(&room_a, cx_a),
6099 RoomParticipants {
6100 remote: Default::default(),
6101 pending: vec!["user_b".to_string()]
6102 }
6103 );
6104
6105 // User B receives the call.
6106
6107 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
6108 let call_b = incoming_call_b.next().await.unwrap().unwrap();
6109 assert_eq!(call_b.calling_user.github_login, "user_a");
6110
6111 // User A shares their screen
6112 let display = gpui::TestScreenCaptureSource::new();
6113 cx_a.set_screen_capture_sources(vec![display]);
6114 active_call_a
6115 .update(cx_a, |call, cx| {
6116 call.room()
6117 .unwrap()
6118 .update(cx, |room, cx| room.share_screen(cx))
6119 })
6120 .await
6121 .unwrap();
6122
6123 client_b.user_store().update(cx_b, |user_store, _| {
6124 user_store.clear_cache();
6125 });
6126
6127 // User B joins the room
6128 active_call_b
6129 .update(cx_b, |call, cx| call.accept_incoming(cx))
6130 .await
6131 .unwrap();
6132
6133 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
6134 assert!(incoming_call_b.next().await.unwrap().is_none());
6135
6136 executor.run_until_parked();
6137 assert_eq!(
6138 room_participants(&room_a, cx_a),
6139 RoomParticipants {
6140 remote: vec!["user_b".to_string()],
6141 pending: vec![],
6142 }
6143 );
6144 assert_eq!(
6145 room_participants(&room_b, cx_b),
6146 RoomParticipants {
6147 remote: vec!["user_a".to_string()],
6148 pending: vec![],
6149 }
6150 );
6151
6152 // Ensure User B sees User A's screenshare.
6153
6154 room_b.read_with(cx_b, |room, _| {
6155 assert_eq!(
6156 room.remote_participants()
6157 .get(&client_a.user_id().unwrap())
6158 .unwrap()
6159 .video_tracks
6160 .len(),
6161 1
6162 );
6163 });
6164}
6165
6166#[gpui::test]
6167async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
6168 let mut server = TestServer::start(cx.executor().clone()).await;
6169 let client_a = server.create_client(cx, "user_a").await;
6170 let (_workspace_a, cx) = client_a.build_test_workspace(cx).await;
6171
6172 cx.simulate_resize(size(px(300.), px(300.)));
6173
6174 cx.simulate_keystrokes("cmd-n cmd-n cmd-n");
6175 cx.update(|cx| cx.refresh());
6176
6177 let tab_bounds = cx.debug_bounds("TAB-2").unwrap();
6178 let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap();
6179
6180 assert!(
6181 tab_bounds.intersects(&new_tab_button_bounds),
6182 "Tab should overlap with the new tab button, if this is failing check if there's been a redesign!"
6183 );
6184
6185 cx.simulate_event(MouseDownEvent {
6186 button: MouseButton::Right,
6187 position: new_tab_button_bounds.center(),
6188 modifiers: Modifiers::default(),
6189 click_count: 1,
6190 first_mouse: false,
6191 });
6192
6193 // regression test that the right click menu for tabs does not open.
6194 assert!(cx.debug_bounds("MENU_ITEM-Close").is_none());
6195
6196 let tab_bounds = cx.debug_bounds("TAB-1").unwrap();
6197 cx.simulate_event(MouseDownEvent {
6198 button: MouseButton::Right,
6199 position: tab_bounds.center(),
6200 modifiers: Modifiers::default(),
6201 click_count: 1,
6202 first_mouse: false,
6203 });
6204 assert!(cx.debug_bounds("MENU_ITEM-Close").is_some());
6205}
6206
6207#[gpui::test]
6208async fn test_pane_split_left(cx: &mut TestAppContext) {
6209 let (_, client) = TestServer::start1(cx).await;
6210 let (workspace, cx) = client.build_test_workspace(cx).await;
6211
6212 cx.simulate_keystrokes("cmd-n");
6213 workspace.update(cx, |workspace, cx| {
6214 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 1);
6215 });
6216 cx.simulate_keystrokes("cmd-k left");
6217 workspace.update(cx, |workspace, cx| {
6218 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
6219 });
6220 cx.simulate_keystrokes("cmd-k");
6221 // sleep for longer than the timeout in keyboard shortcut handling
6222 // to verify that it doesn't fire in this case.
6223 cx.executor().advance_clock(Duration::from_secs(2));
6224 cx.simulate_keystrokes("left");
6225 workspace.update(cx, |workspace, cx| {
6226 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
6227 });
6228}
6229
6230#[gpui::test]
6231async fn test_join_after_restart(cx1: &mut TestAppContext, cx2: &mut TestAppContext) {
6232 let (mut server, client) = TestServer::start1(cx1).await;
6233 let channel1 = server.make_public_channel("channel1", &client, cx1).await;
6234 let channel2 = server.make_public_channel("channel2", &client, cx1).await;
6235
6236 join_channel(channel1, &client, cx1).await.unwrap();
6237 drop(client);
6238
6239 let client2 = server.create_client(cx2, "user_a").await;
6240 join_channel(channel2, &client2, cx2).await.unwrap();
6241}
6242
6243#[gpui::test]
6244async fn test_preview_tabs(cx: &mut TestAppContext) {
6245 let (_server, client) = TestServer::start1(cx).await;
6246 let (workspace, cx) = client.build_test_workspace(cx).await;
6247 let project = workspace.update(cx, |workspace, _| workspace.project().clone());
6248
6249 let worktree_id = project.update(cx, |project, cx| {
6250 project.worktrees(cx).next().unwrap().read(cx).id()
6251 });
6252
6253 let path_1 = ProjectPath {
6254 worktree_id,
6255 path: Path::new("1.txt").into(),
6256 };
6257 let path_2 = ProjectPath {
6258 worktree_id,
6259 path: Path::new("2.js").into(),
6260 };
6261 let path_3 = ProjectPath {
6262 worktree_id,
6263 path: Path::new("3.rs").into(),
6264 };
6265
6266 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6267
6268 let get_path = |pane: &Pane, idx: usize, cx: &AppContext| {
6269 pane.item_for_index(idx).unwrap().project_path(cx).unwrap()
6270 };
6271
6272 // Opening item 3 as a "permanent" tab
6273 workspace
6274 .update(cx, |workspace, cx| {
6275 workspace.open_path(path_3.clone(), None, false, cx)
6276 })
6277 .await
6278 .unwrap();
6279
6280 pane.update(cx, |pane, cx| {
6281 assert_eq!(pane.items_len(), 1);
6282 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6283 assert_eq!(pane.preview_item_id(), None);
6284
6285 assert!(!pane.can_navigate_backward());
6286 assert!(!pane.can_navigate_forward());
6287 });
6288
6289 // Open item 1 as preview
6290 workspace
6291 .update(cx, |workspace, cx| {
6292 workspace.open_path_preview(path_1.clone(), None, true, true, cx)
6293 })
6294 .await
6295 .unwrap();
6296
6297 pane.update(cx, |pane, cx| {
6298 assert_eq!(pane.items_len(), 2);
6299 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6300 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6301 assert_eq!(
6302 pane.preview_item_id(),
6303 Some(pane.items().nth(1).unwrap().item_id())
6304 );
6305
6306 assert!(pane.can_navigate_backward());
6307 assert!(!pane.can_navigate_forward());
6308 });
6309
6310 // Open item 2 as preview
6311 workspace
6312 .update(cx, |workspace, cx| {
6313 workspace.open_path_preview(path_2.clone(), None, true, true, cx)
6314 })
6315 .await
6316 .unwrap();
6317
6318 pane.update(cx, |pane, cx| {
6319 assert_eq!(pane.items_len(), 2);
6320 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6321 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6322 assert_eq!(
6323 pane.preview_item_id(),
6324 Some(pane.items().nth(1).unwrap().item_id())
6325 );
6326
6327 assert!(pane.can_navigate_backward());
6328 assert!(!pane.can_navigate_forward());
6329 });
6330
6331 // Going back should show item 1 as preview
6332 workspace
6333 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6334 .await
6335 .unwrap();
6336
6337 pane.update(cx, |pane, cx| {
6338 assert_eq!(pane.items_len(), 2);
6339 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6340 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6341 assert_eq!(
6342 pane.preview_item_id(),
6343 Some(pane.items().nth(1).unwrap().item_id())
6344 );
6345
6346 assert!(pane.can_navigate_backward());
6347 assert!(pane.can_navigate_forward());
6348 });
6349
6350 // Closing item 1
6351 pane.update(cx, |pane, cx| {
6352 pane.close_item_by_id(
6353 pane.active_item().unwrap().item_id(),
6354 workspace::SaveIntent::Skip,
6355 cx,
6356 )
6357 })
6358 .await
6359 .unwrap();
6360
6361 pane.update(cx, |pane, cx| {
6362 assert_eq!(pane.items_len(), 1);
6363 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6364 assert_eq!(pane.preview_item_id(), None);
6365
6366 assert!(pane.can_navigate_backward());
6367 assert!(!pane.can_navigate_forward());
6368 });
6369
6370 // Going back should show item 1 as preview
6371 workspace
6372 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6373 .await
6374 .unwrap();
6375
6376 pane.update(cx, |pane, cx| {
6377 assert_eq!(pane.items_len(), 2);
6378 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6379 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6380 assert_eq!(
6381 pane.preview_item_id(),
6382 Some(pane.items().nth(1).unwrap().item_id())
6383 );
6384
6385 assert!(pane.can_navigate_backward());
6386 assert!(pane.can_navigate_forward());
6387 });
6388
6389 // Close permanent tab
6390 pane.update(cx, |pane, cx| {
6391 let id = pane.items().next().unwrap().item_id();
6392 pane.close_item_by_id(id, workspace::SaveIntent::Skip, cx)
6393 })
6394 .await
6395 .unwrap();
6396
6397 pane.update(cx, |pane, cx| {
6398 assert_eq!(pane.items_len(), 1);
6399 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6400 assert_eq!(
6401 pane.preview_item_id(),
6402 Some(pane.items().next().unwrap().item_id())
6403 );
6404
6405 assert!(pane.can_navigate_backward());
6406 assert!(pane.can_navigate_forward());
6407 });
6408
6409 // Split pane to the right
6410 pane.update(cx, |pane, cx| {
6411 pane.split(workspace::SplitDirection::Right, cx);
6412 });
6413
6414 let right_pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6415
6416 pane.update(cx, |pane, cx| {
6417 assert_eq!(pane.items_len(), 1);
6418 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6419 assert_eq!(
6420 pane.preview_item_id(),
6421 Some(pane.items().next().unwrap().item_id())
6422 );
6423
6424 assert!(pane.can_navigate_backward());
6425 assert!(pane.can_navigate_forward());
6426 });
6427
6428 right_pane.update(cx, |pane, cx| {
6429 assert_eq!(pane.items_len(), 1);
6430 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6431 assert_eq!(pane.preview_item_id(), None);
6432
6433 assert!(!pane.can_navigate_backward());
6434 assert!(!pane.can_navigate_forward());
6435 });
6436
6437 // Open item 2 as preview in right pane
6438 workspace
6439 .update(cx, |workspace, cx| {
6440 workspace.open_path_preview(path_2.clone(), None, true, true, cx)
6441 })
6442 .await
6443 .unwrap();
6444
6445 pane.update(cx, |pane, cx| {
6446 assert_eq!(pane.items_len(), 1);
6447 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6448 assert_eq!(
6449 pane.preview_item_id(),
6450 Some(pane.items().next().unwrap().item_id())
6451 );
6452
6453 assert!(pane.can_navigate_backward());
6454 assert!(pane.can_navigate_forward());
6455 });
6456
6457 right_pane.update(cx, |pane, cx| {
6458 assert_eq!(pane.items_len(), 2);
6459 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6460 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6461 assert_eq!(
6462 pane.preview_item_id(),
6463 Some(pane.items().nth(1).unwrap().item_id())
6464 );
6465
6466 assert!(pane.can_navigate_backward());
6467 assert!(!pane.can_navigate_forward());
6468 });
6469
6470 // Focus left pane
6471 workspace.update(cx, |workspace, cx| {
6472 workspace.activate_pane_in_direction(workspace::SplitDirection::Left, cx)
6473 });
6474
6475 // Open item 2 as preview in left pane
6476 workspace
6477 .update(cx, |workspace, cx| {
6478 workspace.open_path_preview(path_2.clone(), None, true, true, cx)
6479 })
6480 .await
6481 .unwrap();
6482
6483 pane.update(cx, |pane, cx| {
6484 assert_eq!(pane.items_len(), 1);
6485 assert_eq!(get_path(pane, 0, cx), path_2.clone());
6486 assert_eq!(
6487 pane.preview_item_id(),
6488 Some(pane.items().next().unwrap().item_id())
6489 );
6490
6491 assert!(pane.can_navigate_backward());
6492 assert!(!pane.can_navigate_forward());
6493 });
6494
6495 right_pane.update(cx, |pane, cx| {
6496 assert_eq!(pane.items_len(), 2);
6497 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6498 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6499 assert_eq!(
6500 pane.preview_item_id(),
6501 Some(pane.items().nth(1).unwrap().item_id())
6502 );
6503
6504 assert!(pane.can_navigate_backward());
6505 assert!(!pane.can_navigate_forward());
6506 });
6507}
6508
6509#[gpui::test(iterations = 10)]
6510async fn test_context_collaboration_with_reconnect(
6511 executor: BackgroundExecutor,
6512 cx_a: &mut TestAppContext,
6513 cx_b: &mut TestAppContext,
6514) {
6515 let mut server = TestServer::start(executor.clone()).await;
6516 let client_a = server.create_client(cx_a, "user_a").await;
6517 let client_b = server.create_client(cx_b, "user_b").await;
6518 server
6519 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6520 .await;
6521 let active_call_a = cx_a.read(ActiveCall::global);
6522
6523 client_a.fs().insert_tree("/a", Default::default()).await;
6524 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
6525 let project_id = active_call_a
6526 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6527 .await
6528 .unwrap();
6529 let project_b = client_b.join_remote_project(project_id, cx_b).await;
6530
6531 // Client A sees that a guest has joined.
6532 executor.run_until_parked();
6533
6534 project_a.read_with(cx_a, |project, _| {
6535 assert_eq!(project.collaborators().len(), 1);
6536 });
6537 project_b.read_with(cx_b, |project, _| {
6538 assert_eq!(project.collaborators().len(), 1);
6539 });
6540
6541 cx_a.update(context_server::init);
6542 cx_b.update(context_server::init);
6543 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
6544 let context_store_a = cx_a
6545 .update(|cx| {
6546 ContextStore::new(
6547 project_a.clone(),
6548 prompt_builder.clone(),
6549 Arc::new(SlashCommandWorkingSet::default()),
6550 Arc::new(ToolWorkingSet::default()),
6551 cx,
6552 )
6553 })
6554 .await
6555 .unwrap();
6556 let context_store_b = cx_b
6557 .update(|cx| {
6558 ContextStore::new(
6559 project_b.clone(),
6560 prompt_builder.clone(),
6561 Arc::new(SlashCommandWorkingSet::default()),
6562 Arc::new(ToolWorkingSet::default()),
6563 cx,
6564 )
6565 })
6566 .await
6567 .unwrap();
6568
6569 // Client A creates a new chats.
6570 let context_a = context_store_a.update(cx_a, |store, cx| store.create(cx));
6571 executor.run_until_parked();
6572
6573 // Client B retrieves host's contexts and joins one.
6574 let context_b = context_store_b
6575 .update(cx_b, |store, cx| {
6576 let host_contexts = store.host_contexts().to_vec();
6577 assert_eq!(host_contexts.len(), 1);
6578 store.open_remote_context(host_contexts[0].id.clone(), cx)
6579 })
6580 .await
6581 .unwrap();
6582
6583 // Host and guest make changes
6584 context_a.update(cx_a, |context, cx| {
6585 context.buffer().update(cx, |buffer, cx| {
6586 buffer.edit([(0..0, "Host change\n")], None, cx)
6587 })
6588 });
6589 context_b.update(cx_b, |context, cx| {
6590 context.buffer().update(cx, |buffer, cx| {
6591 buffer.edit([(0..0, "Guest change\n")], None, cx)
6592 })
6593 });
6594 executor.run_until_parked();
6595 assert_eq!(
6596 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6597 "Guest change\nHost change\n"
6598 );
6599 assert_eq!(
6600 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6601 "Guest change\nHost change\n"
6602 );
6603
6604 // Disconnect client A and make some changes while disconnected.
6605 server.disconnect_client(client_a.peer_id().unwrap());
6606 server.forbid_connections();
6607 context_a.update(cx_a, |context, cx| {
6608 context.buffer().update(cx, |buffer, cx| {
6609 buffer.edit([(0..0, "Host offline change\n")], None, cx)
6610 })
6611 });
6612 context_b.update(cx_b, |context, cx| {
6613 context.buffer().update(cx, |buffer, cx| {
6614 buffer.edit([(0..0, "Guest offline change\n")], None, cx)
6615 })
6616 });
6617 executor.run_until_parked();
6618 assert_eq!(
6619 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6620 "Host offline change\nGuest change\nHost change\n"
6621 );
6622 assert_eq!(
6623 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6624 "Guest offline change\nGuest change\nHost change\n"
6625 );
6626
6627 // Allow client A to reconnect and verify that contexts converge.
6628 server.allow_connections();
6629 executor.advance_clock(RECEIVE_TIMEOUT);
6630 assert_eq!(
6631 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6632 "Guest offline change\nHost offline change\nGuest change\nHost change\n"
6633 );
6634 assert_eq!(
6635 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6636 "Guest offline change\nHost offline change\nGuest change\nHost change\n"
6637 );
6638
6639 // Client A disconnects without being able to reconnect. Context B becomes readonly.
6640 server.forbid_connections();
6641 server.disconnect_client(client_a.peer_id().unwrap());
6642 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
6643 context_b.read_with(cx_b, |context, cx| {
6644 assert!(context.buffer().read(cx).read_only());
6645 });
6646}
6647
6648#[gpui::test]
6649async fn test_remote_git_branches(
6650 executor: BackgroundExecutor,
6651 cx_a: &mut TestAppContext,
6652 cx_b: &mut TestAppContext,
6653) {
6654 let mut server = TestServer::start(executor.clone()).await;
6655 let client_a = server.create_client(cx_a, "user_a").await;
6656 let client_b = server.create_client(cx_b, "user_b").await;
6657 server
6658 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6659 .await;
6660 let active_call_a = cx_a.read(ActiveCall::global);
6661
6662 client_a
6663 .fs()
6664 .insert_tree("/project", serde_json::json!({ ".git":{} }))
6665 .await;
6666 let branches = ["main", "dev", "feature-1"];
6667 client_a
6668 .fs()
6669 .insert_branches(Path::new("/project/.git"), &branches);
6670 let branches_set = branches
6671 .into_iter()
6672 .map(ToString::to_string)
6673 .collect::<HashSet<_>>();
6674
6675 let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await;
6676 let project_id = active_call_a
6677 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6678 .await
6679 .unwrap();
6680 let project_b = client_b.join_remote_project(project_id, cx_b).await;
6681
6682 let root_path = ProjectPath::root_path(worktree_id);
6683 // Client A sees that a guest has joined.
6684 executor.run_until_parked();
6685
6686 let branches_b = cx_b
6687 .update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
6688 .await
6689 .unwrap();
6690
6691 let new_branch = branches[2];
6692
6693 let branches_b = branches_b
6694 .into_iter()
6695 .map(|branch| branch.name.to_string())
6696 .collect::<HashSet<_>>();
6697
6698 assert_eq!(branches_b, branches_set);
6699
6700 cx_b.update(|cx| {
6701 project_b.update(cx, |project, cx| {
6702 project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
6703 })
6704 })
6705 .await
6706 .unwrap();
6707
6708 executor.run_until_parked();
6709
6710 let host_branch = cx_a.update(|cx| {
6711 project_a.update(cx, |project, cx| {
6712 project.worktree_store().update(cx, |worktree_store, cx| {
6713 worktree_store
6714 .current_branch(root_path.clone(), cx)
6715 .unwrap()
6716 })
6717 })
6718 });
6719
6720 assert_eq!(host_branch.as_ref(), branches[2]);
6721
6722 // Also try creating a new branch
6723 cx_b.update(|cx| {
6724 project_b.update(cx, |project, cx| {
6725 project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
6726 })
6727 })
6728 .await
6729 .unwrap();
6730
6731 executor.run_until_parked();
6732
6733 let host_branch = cx_a.update(|cx| {
6734 project_a.update(cx, |project, cx| {
6735 project.worktree_store().update(cx, |worktree_store, cx| {
6736 worktree_store.current_branch(root_path, cx).unwrap()
6737 })
6738 })
6739 });
6740
6741 assert_eq!(host_branch.as_ref(), "totally-new-branch");
6742}