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 // Smoke test status reading
2929
2930 project_local.read_with(cx_a, |project, cx| {
2931 assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
2932 assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
2933 });
2934
2935 project_remote.read_with(cx_b, |project, cx| {
2936 assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
2937 assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
2938 });
2939
2940 client_a.fs().set_status_for_repo_via_working_copy_change(
2941 Path::new("/dir/.git"),
2942 &[
2943 (Path::new(A_TXT), GitFileStatus::Modified),
2944 (Path::new(B_TXT), GitFileStatus::Modified),
2945 ],
2946 );
2947
2948 // Wait for buffer_local_a to receive it
2949 executor.run_until_parked();
2950
2951 // Smoke test status reading
2952
2953 project_local.read_with(cx_a, |project, cx| {
2954 assert_status(
2955 &Path::new(A_TXT),
2956 Some(GitFileStatus::Modified),
2957 project,
2958 cx,
2959 );
2960 assert_status(
2961 &Path::new(B_TXT),
2962 Some(GitFileStatus::Modified),
2963 project,
2964 cx,
2965 );
2966 });
2967
2968 project_remote.read_with(cx_b, |project, cx| {
2969 assert_status(
2970 &Path::new(A_TXT),
2971 Some(GitFileStatus::Modified),
2972 project,
2973 cx,
2974 );
2975 assert_status(
2976 &Path::new(B_TXT),
2977 Some(GitFileStatus::Modified),
2978 project,
2979 cx,
2980 );
2981 });
2982
2983 // And synchronization while joining
2984 let project_remote_c = client_c.join_remote_project(project_id, cx_c).await;
2985 executor.run_until_parked();
2986
2987 project_remote_c.read_with(cx_c, |project, cx| {
2988 assert_status(
2989 &Path::new(A_TXT),
2990 Some(GitFileStatus::Modified),
2991 project,
2992 cx,
2993 );
2994 assert_status(
2995 &Path::new(B_TXT),
2996 Some(GitFileStatus::Modified),
2997 project,
2998 cx,
2999 );
3000 });
3001}
3002
3003#[gpui::test(iterations = 10)]
3004async fn test_fs_operations(
3005 executor: BackgroundExecutor,
3006 cx_a: &mut TestAppContext,
3007 cx_b: &mut TestAppContext,
3008) {
3009 let mut server = TestServer::start(executor.clone()).await;
3010 let client_a = server.create_client(cx_a, "user_a").await;
3011 let client_b = server.create_client(cx_b, "user_b").await;
3012 server
3013 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3014 .await;
3015 let active_call_a = cx_a.read(ActiveCall::global);
3016
3017 client_a
3018 .fs()
3019 .insert_tree(
3020 "/dir",
3021 json!({
3022 "a.txt": "a-contents",
3023 "b.txt": "b-contents",
3024 }),
3025 )
3026 .await;
3027 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3028 let project_id = active_call_a
3029 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3030 .await
3031 .unwrap();
3032 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3033
3034 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
3035 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
3036
3037 let entry = project_b
3038 .update(cx_b, |project, cx| {
3039 project.create_entry((worktree_id, "c.txt"), false, cx)
3040 })
3041 .await
3042 .unwrap()
3043 .to_included()
3044 .unwrap();
3045
3046 worktree_a.read_with(cx_a, |worktree, _| {
3047 assert_eq!(
3048 worktree
3049 .paths()
3050 .map(|p| p.to_string_lossy())
3051 .collect::<Vec<_>>(),
3052 ["a.txt", "b.txt", "c.txt"]
3053 );
3054 });
3055
3056 worktree_b.read_with(cx_b, |worktree, _| {
3057 assert_eq!(
3058 worktree
3059 .paths()
3060 .map(|p| p.to_string_lossy())
3061 .collect::<Vec<_>>(),
3062 ["a.txt", "b.txt", "c.txt"]
3063 );
3064 });
3065
3066 project_b
3067 .update(cx_b, |project, cx| {
3068 project.rename_entry(entry.id, Path::new("d.txt"), cx)
3069 })
3070 .await
3071 .unwrap()
3072 .to_included()
3073 .unwrap();
3074
3075 worktree_a.read_with(cx_a, |worktree, _| {
3076 assert_eq!(
3077 worktree
3078 .paths()
3079 .map(|p| p.to_string_lossy())
3080 .collect::<Vec<_>>(),
3081 ["a.txt", "b.txt", "d.txt"]
3082 );
3083 });
3084
3085 worktree_b.read_with(cx_b, |worktree, _| {
3086 assert_eq!(
3087 worktree
3088 .paths()
3089 .map(|p| p.to_string_lossy())
3090 .collect::<Vec<_>>(),
3091 ["a.txt", "b.txt", "d.txt"]
3092 );
3093 });
3094
3095 let dir_entry = project_b
3096 .update(cx_b, |project, cx| {
3097 project.create_entry((worktree_id, "DIR"), true, cx)
3098 })
3099 .await
3100 .unwrap()
3101 .to_included()
3102 .unwrap();
3103
3104 worktree_a.read_with(cx_a, |worktree, _| {
3105 assert_eq!(
3106 worktree
3107 .paths()
3108 .map(|p| p.to_string_lossy())
3109 .collect::<Vec<_>>(),
3110 ["DIR", "a.txt", "b.txt", "d.txt"]
3111 );
3112 });
3113
3114 worktree_b.read_with(cx_b, |worktree, _| {
3115 assert_eq!(
3116 worktree
3117 .paths()
3118 .map(|p| p.to_string_lossy())
3119 .collect::<Vec<_>>(),
3120 ["DIR", "a.txt", "b.txt", "d.txt"]
3121 );
3122 });
3123
3124 project_b
3125 .update(cx_b, |project, cx| {
3126 project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
3127 })
3128 .await
3129 .unwrap()
3130 .to_included()
3131 .unwrap();
3132
3133 project_b
3134 .update(cx_b, |project, cx| {
3135 project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
3136 })
3137 .await
3138 .unwrap()
3139 .to_included()
3140 .unwrap();
3141
3142 project_b
3143 .update(cx_b, |project, cx| {
3144 project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
3145 })
3146 .await
3147 .unwrap()
3148 .to_included()
3149 .unwrap();
3150
3151 worktree_a.read_with(cx_a, |worktree, _| {
3152 assert_eq!(
3153 worktree
3154 .paths()
3155 .map(|p| p.to_string_lossy())
3156 .collect::<Vec<_>>(),
3157 [
3158 "DIR",
3159 "DIR/SUBDIR",
3160 "DIR/SUBDIR/f.txt",
3161 "DIR/e.txt",
3162 "a.txt",
3163 "b.txt",
3164 "d.txt"
3165 ]
3166 );
3167 });
3168
3169 worktree_b.read_with(cx_b, |worktree, _| {
3170 assert_eq!(
3171 worktree
3172 .paths()
3173 .map(|p| p.to_string_lossy())
3174 .collect::<Vec<_>>(),
3175 [
3176 "DIR",
3177 "DIR/SUBDIR",
3178 "DIR/SUBDIR/f.txt",
3179 "DIR/e.txt",
3180 "a.txt",
3181 "b.txt",
3182 "d.txt"
3183 ]
3184 );
3185 });
3186
3187 project_b
3188 .update(cx_b, |project, cx| {
3189 project.copy_entry(entry.id, None, Path::new("f.txt"), cx)
3190 })
3191 .await
3192 .unwrap()
3193 .unwrap();
3194
3195 worktree_a.read_with(cx_a, |worktree, _| {
3196 assert_eq!(
3197 worktree
3198 .paths()
3199 .map(|p| p.to_string_lossy())
3200 .collect::<Vec<_>>(),
3201 [
3202 "DIR",
3203 "DIR/SUBDIR",
3204 "DIR/SUBDIR/f.txt",
3205 "DIR/e.txt",
3206 "a.txt",
3207 "b.txt",
3208 "d.txt",
3209 "f.txt"
3210 ]
3211 );
3212 });
3213
3214 worktree_b.read_with(cx_b, |worktree, _| {
3215 assert_eq!(
3216 worktree
3217 .paths()
3218 .map(|p| p.to_string_lossy())
3219 .collect::<Vec<_>>(),
3220 [
3221 "DIR",
3222 "DIR/SUBDIR",
3223 "DIR/SUBDIR/f.txt",
3224 "DIR/e.txt",
3225 "a.txt",
3226 "b.txt",
3227 "d.txt",
3228 "f.txt"
3229 ]
3230 );
3231 });
3232
3233 project_b
3234 .update(cx_b, |project, cx| {
3235 project.delete_entry(dir_entry.id, false, cx).unwrap()
3236 })
3237 .await
3238 .unwrap();
3239 executor.run_until_parked();
3240
3241 worktree_a.read_with(cx_a, |worktree, _| {
3242 assert_eq!(
3243 worktree
3244 .paths()
3245 .map(|p| p.to_string_lossy())
3246 .collect::<Vec<_>>(),
3247 ["a.txt", "b.txt", "d.txt", "f.txt"]
3248 );
3249 });
3250
3251 worktree_b.read_with(cx_b, |worktree, _| {
3252 assert_eq!(
3253 worktree
3254 .paths()
3255 .map(|p| p.to_string_lossy())
3256 .collect::<Vec<_>>(),
3257 ["a.txt", "b.txt", "d.txt", "f.txt"]
3258 );
3259 });
3260
3261 project_b
3262 .update(cx_b, |project, cx| {
3263 project.delete_entry(entry.id, false, cx).unwrap()
3264 })
3265 .await
3266 .unwrap();
3267
3268 worktree_a.read_with(cx_a, |worktree, _| {
3269 assert_eq!(
3270 worktree
3271 .paths()
3272 .map(|p| p.to_string_lossy())
3273 .collect::<Vec<_>>(),
3274 ["a.txt", "b.txt", "f.txt"]
3275 );
3276 });
3277
3278 worktree_b.read_with(cx_b, |worktree, _| {
3279 assert_eq!(
3280 worktree
3281 .paths()
3282 .map(|p| p.to_string_lossy())
3283 .collect::<Vec<_>>(),
3284 ["a.txt", "b.txt", "f.txt"]
3285 );
3286 });
3287}
3288
3289#[gpui::test(iterations = 10)]
3290async fn test_local_settings(
3291 executor: BackgroundExecutor,
3292 cx_a: &mut TestAppContext,
3293 cx_b: &mut TestAppContext,
3294) {
3295 let mut server = TestServer::start(executor.clone()).await;
3296 let client_a = server.create_client(cx_a, "user_a").await;
3297 let client_b = server.create_client(cx_b, "user_b").await;
3298 server
3299 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3300 .await;
3301 let active_call_a = cx_a.read(ActiveCall::global);
3302
3303 // As client A, open a project that contains some local settings files
3304 client_a
3305 .fs()
3306 .insert_tree(
3307 "/dir",
3308 json!({
3309 ".zed": {
3310 "settings.json": r#"{ "tab_size": 2 }"#
3311 },
3312 "a": {
3313 ".zed": {
3314 "settings.json": r#"{ "tab_size": 8 }"#
3315 },
3316 "a.txt": "a-contents",
3317 },
3318 "b": {
3319 "b.txt": "b-contents",
3320 }
3321 }),
3322 )
3323 .await;
3324 let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
3325 executor.run_until_parked();
3326 let project_id = active_call_a
3327 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3328 .await
3329 .unwrap();
3330 executor.run_until_parked();
3331
3332 // As client B, join that project and observe the local settings.
3333 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3334
3335 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
3336 executor.run_until_parked();
3337 cx_b.read(|cx| {
3338 let store = cx.global::<SettingsStore>();
3339 assert_eq!(
3340 store
3341 .local_settings(worktree_b.read(cx).id())
3342 .collect::<Vec<_>>(),
3343 &[
3344 (Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
3345 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3346 ]
3347 )
3348 });
3349
3350 // As client A, update a settings file. As Client B, see the changed settings.
3351 client_a
3352 .fs()
3353 .insert_file("/dir/.zed/settings.json", r#"{}"#.into())
3354 .await;
3355 executor.run_until_parked();
3356 cx_b.read(|cx| {
3357 let store = cx.global::<SettingsStore>();
3358 assert_eq!(
3359 store
3360 .local_settings(worktree_b.read(cx).id())
3361 .collect::<Vec<_>>(),
3362 &[
3363 (Path::new("").into(), r#"{}"#.to_string()),
3364 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3365 ]
3366 )
3367 });
3368
3369 // As client A, create and remove some settings files. As client B, see the changed settings.
3370 client_a
3371 .fs()
3372 .remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
3373 .await
3374 .unwrap();
3375 client_a
3376 .fs()
3377 .create_dir("/dir/b/.zed".as_ref())
3378 .await
3379 .unwrap();
3380 client_a
3381 .fs()
3382 .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
3383 .await;
3384 executor.run_until_parked();
3385 cx_b.read(|cx| {
3386 let store = cx.global::<SettingsStore>();
3387 assert_eq!(
3388 store
3389 .local_settings(worktree_b.read(cx).id())
3390 .collect::<Vec<_>>(),
3391 &[
3392 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3393 (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
3394 ]
3395 )
3396 });
3397
3398 // As client B, disconnect.
3399 server.forbid_connections();
3400 server.disconnect_client(client_b.peer_id().unwrap());
3401
3402 // As client A, change and remove settings files while client B is disconnected.
3403 client_a
3404 .fs()
3405 .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
3406 .await;
3407 client_a
3408 .fs()
3409 .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
3410 .await
3411 .unwrap();
3412 executor.run_until_parked();
3413
3414 // As client B, reconnect and see the changed settings.
3415 server.allow_connections();
3416 executor.advance_clock(RECEIVE_TIMEOUT);
3417 cx_b.read(|cx| {
3418 let store = cx.global::<SettingsStore>();
3419 assert_eq!(
3420 store
3421 .local_settings(worktree_b.read(cx).id())
3422 .collect::<Vec<_>>(),
3423 &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
3424 )
3425 });
3426}
3427
3428#[gpui::test(iterations = 10)]
3429async fn test_buffer_conflict_after_save(
3430 executor: BackgroundExecutor,
3431 cx_a: &mut TestAppContext,
3432 cx_b: &mut TestAppContext,
3433) {
3434 let mut server = TestServer::start(executor.clone()).await;
3435 let client_a = server.create_client(cx_a, "user_a").await;
3436 let client_b = server.create_client(cx_b, "user_b").await;
3437 server
3438 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3439 .await;
3440 let active_call_a = cx_a.read(ActiveCall::global);
3441
3442 client_a
3443 .fs()
3444 .insert_tree(
3445 "/dir",
3446 json!({
3447 "a.txt": "a-contents",
3448 }),
3449 )
3450 .await;
3451 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3452 let project_id = active_call_a
3453 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3454 .await
3455 .unwrap();
3456 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3457
3458 // Open a buffer as client B
3459 let buffer_b = project_b
3460 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3461 .await
3462 .unwrap();
3463
3464 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
3465
3466 buffer_b.read_with(cx_b, |buf, _| {
3467 assert!(buf.is_dirty());
3468 assert!(!buf.has_conflict());
3469 });
3470
3471 project_b
3472 .update(cx_b, |project, cx| {
3473 project.save_buffer(buffer_b.clone(), cx)
3474 })
3475 .await
3476 .unwrap();
3477
3478 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
3479
3480 buffer_b.read_with(cx_b, |buf, _| {
3481 assert!(!buf.has_conflict());
3482 });
3483
3484 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
3485
3486 buffer_b.read_with(cx_b, |buf, _| {
3487 assert!(buf.is_dirty());
3488 assert!(!buf.has_conflict());
3489 });
3490}
3491
3492#[gpui::test(iterations = 10)]
3493async fn test_buffer_reloading(
3494 executor: BackgroundExecutor,
3495 cx_a: &mut TestAppContext,
3496 cx_b: &mut TestAppContext,
3497) {
3498 let mut server = TestServer::start(executor.clone()).await;
3499 let client_a = server.create_client(cx_a, "user_a").await;
3500 let client_b = server.create_client(cx_b, "user_b").await;
3501 server
3502 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3503 .await;
3504 let active_call_a = cx_a.read(ActiveCall::global);
3505
3506 client_a
3507 .fs()
3508 .insert_tree(
3509 "/dir",
3510 json!({
3511 "a.txt": "a\nb\nc",
3512 }),
3513 )
3514 .await;
3515 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3516 let project_id = active_call_a
3517 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3518 .await
3519 .unwrap();
3520 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3521
3522 // Open a buffer as client B
3523 let buffer_b = project_b
3524 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3525 .await
3526 .unwrap();
3527
3528 buffer_b.read_with(cx_b, |buf, _| {
3529 assert!(!buf.is_dirty());
3530 assert!(!buf.has_conflict());
3531 assert_eq!(buf.line_ending(), LineEnding::Unix);
3532 });
3533
3534 let new_contents = Rope::from("d\ne\nf");
3535 client_a
3536 .fs()
3537 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
3538 .await
3539 .unwrap();
3540
3541 executor.run_until_parked();
3542
3543 buffer_b.read_with(cx_b, |buf, _| {
3544 assert_eq!(buf.text(), new_contents.to_string());
3545 assert!(!buf.is_dirty());
3546 assert!(!buf.has_conflict());
3547 assert_eq!(buf.line_ending(), LineEnding::Windows);
3548 });
3549}
3550
3551#[gpui::test(iterations = 10)]
3552async fn test_editing_while_guest_opens_buffer(
3553 executor: BackgroundExecutor,
3554 cx_a: &mut TestAppContext,
3555 cx_b: &mut TestAppContext,
3556) {
3557 let mut server = TestServer::start(executor.clone()).await;
3558 let client_a = server.create_client(cx_a, "user_a").await;
3559 let client_b = server.create_client(cx_b, "user_b").await;
3560 server
3561 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3562 .await;
3563 let active_call_a = cx_a.read(ActiveCall::global);
3564
3565 client_a
3566 .fs()
3567 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3568 .await;
3569 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3570 let project_id = active_call_a
3571 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3572 .await
3573 .unwrap();
3574 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3575
3576 // Open a buffer as client A
3577 let buffer_a = project_a
3578 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3579 .await
3580 .unwrap();
3581
3582 // Start opening the same buffer as client B
3583 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3584 let buffer_b = cx_b.executor().spawn(open_buffer);
3585
3586 // Edit the buffer as client A while client B is still opening it.
3587 cx_b.executor().simulate_random_delay().await;
3588 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
3589 cx_b.executor().simulate_random_delay().await;
3590 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
3591
3592 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3593 let buffer_b = buffer_b.await.unwrap();
3594 executor.run_until_parked();
3595
3596 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3597}
3598
3599#[gpui::test(iterations = 10)]
3600async fn test_leaving_worktree_while_opening_buffer(
3601 executor: BackgroundExecutor,
3602 cx_a: &mut TestAppContext,
3603 cx_b: &mut TestAppContext,
3604) {
3605 let mut server = TestServer::start(executor.clone()).await;
3606 let client_a = server.create_client(cx_a, "user_a").await;
3607 let client_b = server.create_client(cx_b, "user_b").await;
3608 server
3609 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3610 .await;
3611 let active_call_a = cx_a.read(ActiveCall::global);
3612
3613 client_a
3614 .fs()
3615 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3616 .await;
3617 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3618 let project_id = active_call_a
3619 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3620 .await
3621 .unwrap();
3622 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3623
3624 // See that a guest has joined as client A.
3625 executor.run_until_parked();
3626
3627 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3628
3629 // Begin opening a buffer as client B, but leave the project before the open completes.
3630 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3631 let buffer_b = cx_b.executor().spawn(open_buffer);
3632 cx_b.update(|_| drop(project_b));
3633 drop(buffer_b);
3634
3635 // See that the guest has left.
3636 executor.run_until_parked();
3637
3638 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3639}
3640
3641#[gpui::test(iterations = 10)]
3642async fn test_canceling_buffer_opening(
3643 executor: BackgroundExecutor,
3644 cx_a: &mut TestAppContext,
3645 cx_b: &mut TestAppContext,
3646) {
3647 let mut server = TestServer::start(executor.clone()).await;
3648 let client_a = server.create_client(cx_a, "user_a").await;
3649 let client_b = server.create_client(cx_b, "user_b").await;
3650 server
3651 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3652 .await;
3653 let active_call_a = cx_a.read(ActiveCall::global);
3654
3655 client_a
3656 .fs()
3657 .insert_tree(
3658 "/dir",
3659 json!({
3660 "a.txt": "abc",
3661 }),
3662 )
3663 .await;
3664 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3665 let project_id = active_call_a
3666 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3667 .await
3668 .unwrap();
3669 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3670
3671 let buffer_a = project_a
3672 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3673 .await
3674 .unwrap();
3675
3676 // Open a buffer as client B but cancel after a random amount of time.
3677 let buffer_b = project_b.update(cx_b, |p, cx| {
3678 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3679 });
3680 executor.simulate_random_delay().await;
3681 drop(buffer_b);
3682
3683 // Try opening the same buffer again as client B, and ensure we can
3684 // still do it despite the cancellation above.
3685 let buffer_b = project_b
3686 .update(cx_b, |p, cx| {
3687 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3688 })
3689 .await
3690 .unwrap();
3691
3692 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3693}
3694
3695#[gpui::test(iterations = 10)]
3696async fn test_leaving_project(
3697 executor: BackgroundExecutor,
3698 cx_a: &mut TestAppContext,
3699 cx_b: &mut TestAppContext,
3700 cx_c: &mut TestAppContext,
3701) {
3702 let mut server = TestServer::start(executor.clone()).await;
3703 let client_a = server.create_client(cx_a, "user_a").await;
3704 let client_b = server.create_client(cx_b, "user_b").await;
3705 let client_c = server.create_client(cx_c, "user_c").await;
3706 server
3707 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3708 .await;
3709 let active_call_a = cx_a.read(ActiveCall::global);
3710
3711 client_a
3712 .fs()
3713 .insert_tree(
3714 "/a",
3715 json!({
3716 "a.txt": "a-contents",
3717 "b.txt": "b-contents",
3718 }),
3719 )
3720 .await;
3721 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3722 let project_id = active_call_a
3723 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3724 .await
3725 .unwrap();
3726 let project_b1 = client_b.join_remote_project(project_id, cx_b).await;
3727 let project_c = client_c.join_remote_project(project_id, cx_c).await;
3728
3729 // Client A sees that a guest has joined.
3730 executor.run_until_parked();
3731
3732 project_a.read_with(cx_a, |project, _| {
3733 assert_eq!(project.collaborators().len(), 2);
3734 });
3735
3736 project_b1.read_with(cx_b, |project, _| {
3737 assert_eq!(project.collaborators().len(), 2);
3738 });
3739
3740 project_c.read_with(cx_c, |project, _| {
3741 assert_eq!(project.collaborators().len(), 2);
3742 });
3743
3744 // Client B opens a buffer.
3745 let buffer_b1 = project_b1
3746 .update(cx_b, |project, cx| {
3747 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3748 project.open_buffer((worktree_id, "a.txt"), cx)
3749 })
3750 .await
3751 .unwrap();
3752
3753 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3754
3755 // Drop client B's project and ensure client A and client C observe client B leaving.
3756 cx_b.update(|_| drop(project_b1));
3757 executor.run_until_parked();
3758
3759 project_a.read_with(cx_a, |project, _| {
3760 assert_eq!(project.collaborators().len(), 1);
3761 });
3762
3763 project_c.read_with(cx_c, |project, _| {
3764 assert_eq!(project.collaborators().len(), 1);
3765 });
3766
3767 // Client B re-joins the project and can open buffers as before.
3768 let project_b2 = client_b.join_remote_project(project_id, cx_b).await;
3769 executor.run_until_parked();
3770
3771 project_a.read_with(cx_a, |project, _| {
3772 assert_eq!(project.collaborators().len(), 2);
3773 });
3774
3775 project_b2.read_with(cx_b, |project, _| {
3776 assert_eq!(project.collaborators().len(), 2);
3777 });
3778
3779 project_c.read_with(cx_c, |project, _| {
3780 assert_eq!(project.collaborators().len(), 2);
3781 });
3782
3783 let buffer_b2 = project_b2
3784 .update(cx_b, |project, cx| {
3785 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3786 project.open_buffer((worktree_id, "a.txt"), cx)
3787 })
3788 .await
3789 .unwrap();
3790
3791 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3792
3793 project_a.read_with(cx_a, |project, _| {
3794 assert_eq!(project.collaborators().len(), 2);
3795 });
3796
3797 // Drop client B's connection and ensure client A and client C observe client B leaving.
3798 client_b.disconnect(&cx_b.to_async());
3799 executor.advance_clock(RECONNECT_TIMEOUT);
3800
3801 project_a.read_with(cx_a, |project, _| {
3802 assert_eq!(project.collaborators().len(), 1);
3803 });
3804
3805 project_b2.read_with(cx_b, |project, cx| {
3806 assert!(project.is_disconnected(cx));
3807 });
3808
3809 project_c.read_with(cx_c, |project, _| {
3810 assert_eq!(project.collaborators().len(), 1);
3811 });
3812
3813 // Client B can't join the project, unless they re-join the room.
3814 cx_b.spawn(|cx| {
3815 Project::in_room(
3816 project_id,
3817 client_b.app_state.client.clone(),
3818 client_b.user_store().clone(),
3819 client_b.language_registry().clone(),
3820 FakeFs::new(cx.background_executor().clone()),
3821 cx,
3822 )
3823 })
3824 .await
3825 .unwrap_err();
3826
3827 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3828 client_c.wait_for_current_user(cx_c).await;
3829 server.forbid_connections();
3830 server.disconnect_client(client_c.peer_id().unwrap());
3831 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
3832 executor.run_until_parked();
3833
3834 project_a.read_with(cx_a, |project, _| {
3835 assert_eq!(project.collaborators().len(), 0);
3836 });
3837
3838 project_b2.read_with(cx_b, |project, cx| {
3839 assert!(project.is_disconnected(cx));
3840 });
3841
3842 project_c.read_with(cx_c, |project, cx| {
3843 assert!(project.is_disconnected(cx));
3844 });
3845}
3846
3847#[gpui::test(iterations = 10)]
3848async fn test_collaborating_with_diagnostics(
3849 executor: BackgroundExecutor,
3850 cx_a: &mut TestAppContext,
3851 cx_b: &mut TestAppContext,
3852 cx_c: &mut TestAppContext,
3853) {
3854 let mut server = TestServer::start(executor.clone()).await;
3855 let client_a = server.create_client(cx_a, "user_a").await;
3856 let client_b = server.create_client(cx_b, "user_b").await;
3857 let client_c = server.create_client(cx_c, "user_c").await;
3858 server
3859 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3860 .await;
3861 let active_call_a = cx_a.read(ActiveCall::global);
3862
3863 client_a.language_registry().add(Arc::new(Language::new(
3864 LanguageConfig {
3865 name: "Rust".into(),
3866 matcher: LanguageMatcher {
3867 path_suffixes: vec!["rs".to_string()],
3868 ..Default::default()
3869 },
3870 ..Default::default()
3871 },
3872 Some(tree_sitter_rust::LANGUAGE.into()),
3873 )));
3874 let mut fake_language_servers = client_a
3875 .language_registry()
3876 .register_fake_lsp("Rust", Default::default());
3877
3878 // Share a project as client A
3879 client_a
3880 .fs()
3881 .insert_tree(
3882 "/a",
3883 json!({
3884 "a.rs": "let one = two",
3885 "other.rs": "",
3886 }),
3887 )
3888 .await;
3889 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3890
3891 // Cause the language server to start.
3892 let _buffer = project_a
3893 .update(cx_a, |project, cx| {
3894 project.open_local_buffer_with_lsp("/a/other.rs", cx)
3895 })
3896 .await
3897 .unwrap();
3898
3899 // Simulate a language server reporting errors for a file.
3900 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3901 fake_language_server
3902 .receive_notification::<lsp::notification::DidOpenTextDocument>()
3903 .await;
3904 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3905 lsp::PublishDiagnosticsParams {
3906 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3907 version: None,
3908 diagnostics: vec![lsp::Diagnostic {
3909 severity: Some(lsp::DiagnosticSeverity::WARNING),
3910 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3911 message: "message 0".to_string(),
3912 ..Default::default()
3913 }],
3914 },
3915 );
3916
3917 // Client A shares the project and, simultaneously, the language server
3918 // publishes a diagnostic. This is done to ensure that the server always
3919 // observes the latest diagnostics for a worktree.
3920 let project_id = active_call_a
3921 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3922 .await
3923 .unwrap();
3924 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3925 lsp::PublishDiagnosticsParams {
3926 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3927 version: None,
3928 diagnostics: vec![lsp::Diagnostic {
3929 severity: Some(lsp::DiagnosticSeverity::ERROR),
3930 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3931 message: "message 1".to_string(),
3932 ..Default::default()
3933 }],
3934 },
3935 );
3936
3937 // Join the worktree as client B.
3938 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3939
3940 // Wait for server to see the diagnostics update.
3941 executor.run_until_parked();
3942
3943 // Ensure client B observes the new diagnostics.
3944
3945 project_b.read_with(cx_b, |project, cx| {
3946 assert_eq!(
3947 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3948 &[(
3949 ProjectPath {
3950 worktree_id,
3951 path: Arc::from(Path::new("a.rs")),
3952 },
3953 LanguageServerId(0),
3954 DiagnosticSummary {
3955 error_count: 1,
3956 warning_count: 0,
3957 },
3958 )]
3959 )
3960 });
3961
3962 // Join project as client C and observe the diagnostics.
3963 let project_c = client_c.join_remote_project(project_id, cx_c).await;
3964 executor.run_until_parked();
3965 let project_c_diagnostic_summaries =
3966 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
3967 project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
3968 })));
3969 project_c.update(cx_c, |_, cx| {
3970 let summaries = project_c_diagnostic_summaries.clone();
3971 cx.subscribe(&project_c, {
3972 move |p, _, event, cx| {
3973 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3974 *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
3975 }
3976 }
3977 })
3978 .detach();
3979 });
3980
3981 executor.run_until_parked();
3982 assert_eq!(
3983 project_c_diagnostic_summaries.borrow().as_slice(),
3984 &[(
3985 ProjectPath {
3986 worktree_id,
3987 path: Arc::from(Path::new("a.rs")),
3988 },
3989 LanguageServerId(0),
3990 DiagnosticSummary {
3991 error_count: 1,
3992 warning_count: 0,
3993 },
3994 )]
3995 );
3996
3997 // Simulate a language server reporting more errors for a file.
3998 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3999 lsp::PublishDiagnosticsParams {
4000 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
4001 version: None,
4002 diagnostics: vec![
4003 lsp::Diagnostic {
4004 severity: Some(lsp::DiagnosticSeverity::ERROR),
4005 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
4006 message: "message 1".to_string(),
4007 ..Default::default()
4008 },
4009 lsp::Diagnostic {
4010 severity: Some(lsp::DiagnosticSeverity::WARNING),
4011 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
4012 message: "message 2".to_string(),
4013 ..Default::default()
4014 },
4015 ],
4016 },
4017 );
4018
4019 // Clients B and C get the updated summaries
4020 executor.run_until_parked();
4021
4022 project_b.read_with(cx_b, |project, cx| {
4023 assert_eq!(
4024 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4025 [(
4026 ProjectPath {
4027 worktree_id,
4028 path: Arc::from(Path::new("a.rs")),
4029 },
4030 LanguageServerId(0),
4031 DiagnosticSummary {
4032 error_count: 1,
4033 warning_count: 1,
4034 },
4035 )]
4036 );
4037 });
4038
4039 project_c.read_with(cx_c, |project, cx| {
4040 assert_eq!(
4041 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4042 [(
4043 ProjectPath {
4044 worktree_id,
4045 path: Arc::from(Path::new("a.rs")),
4046 },
4047 LanguageServerId(0),
4048 DiagnosticSummary {
4049 error_count: 1,
4050 warning_count: 1,
4051 },
4052 )]
4053 );
4054 });
4055
4056 // Open the file with the errors on client B. They should be present.
4057 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4058 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4059
4060 buffer_b.read_with(cx_b, |buffer, _| {
4061 assert_eq!(
4062 buffer
4063 .snapshot()
4064 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
4065 .collect::<Vec<_>>(),
4066 &[
4067 DiagnosticEntry {
4068 range: Point::new(0, 4)..Point::new(0, 7),
4069 diagnostic: Diagnostic {
4070 group_id: 2,
4071 message: "message 1".to_string(),
4072 severity: lsp::DiagnosticSeverity::ERROR,
4073 is_primary: true,
4074 ..Default::default()
4075 }
4076 },
4077 DiagnosticEntry {
4078 range: Point::new(0, 10)..Point::new(0, 13),
4079 diagnostic: Diagnostic {
4080 group_id: 3,
4081 severity: lsp::DiagnosticSeverity::WARNING,
4082 message: "message 2".to_string(),
4083 is_primary: true,
4084 ..Default::default()
4085 }
4086 }
4087 ]
4088 );
4089 });
4090
4091 // Simulate a language server reporting no errors for a file.
4092 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4093 lsp::PublishDiagnosticsParams {
4094 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
4095 version: None,
4096 diagnostics: vec![],
4097 },
4098 );
4099 executor.run_until_parked();
4100
4101 project_a.read_with(cx_a, |project, cx| {
4102 assert_eq!(
4103 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4104 []
4105 )
4106 });
4107
4108 project_b.read_with(cx_b, |project, cx| {
4109 assert_eq!(
4110 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4111 []
4112 )
4113 });
4114
4115 project_c.read_with(cx_c, |project, cx| {
4116 assert_eq!(
4117 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4118 []
4119 )
4120 });
4121}
4122
4123#[gpui::test(iterations = 10)]
4124async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
4125 executor: BackgroundExecutor,
4126 cx_a: &mut TestAppContext,
4127 cx_b: &mut TestAppContext,
4128) {
4129 let mut server = TestServer::start(executor.clone()).await;
4130 let client_a = server.create_client(cx_a, "user_a").await;
4131 let client_b = server.create_client(cx_b, "user_b").await;
4132 server
4133 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4134 .await;
4135
4136 client_a.language_registry().add(rust_lang());
4137 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4138 "Rust",
4139 FakeLspAdapter {
4140 disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
4141 disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
4142 ..Default::default()
4143 },
4144 );
4145
4146 let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
4147 client_a
4148 .fs()
4149 .insert_tree(
4150 "/test",
4151 json!({
4152 "one.rs": "const ONE: usize = 1;",
4153 "two.rs": "const TWO: usize = 2;",
4154 "three.rs": "const THREE: usize = 3;",
4155 "four.rs": "const FOUR: usize = 3;",
4156 "five.rs": "const FIVE: usize = 3;",
4157 }),
4158 )
4159 .await;
4160
4161 let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
4162
4163 // Share a project as client A
4164 let active_call_a = cx_a.read(ActiveCall::global);
4165 let project_id = active_call_a
4166 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4167 .await
4168 .unwrap();
4169
4170 // Join the project as client B and open all three files.
4171 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4172 let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
4173 project_b.update(cx_b, |p, cx| {
4174 p.open_buffer_with_lsp((worktree_id, file_name), cx)
4175 })
4176 }))
4177 .await
4178 .unwrap();
4179
4180 // Simulate a language server reporting errors for a file.
4181 let fake_language_server = fake_language_servers.next().await.unwrap();
4182 fake_language_server
4183 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
4184 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4185 })
4186 .await
4187 .unwrap();
4188 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4189 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4190 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
4191 lsp::WorkDoneProgressBegin {
4192 title: "Progress Began".into(),
4193 ..Default::default()
4194 },
4195 )),
4196 });
4197 for file_name in file_names {
4198 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4199 lsp::PublishDiagnosticsParams {
4200 uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
4201 version: None,
4202 diagnostics: vec![lsp::Diagnostic {
4203 severity: Some(lsp::DiagnosticSeverity::WARNING),
4204 source: Some("the-disk-based-diagnostics-source".into()),
4205 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
4206 message: "message one".to_string(),
4207 ..Default::default()
4208 }],
4209 },
4210 );
4211 }
4212 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4213 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4214 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
4215 lsp::WorkDoneProgressEnd { message: None },
4216 )),
4217 });
4218
4219 // When the "disk base diagnostics finished" message is received, the buffers'
4220 // diagnostics are expected to be present.
4221 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
4222 project_b.update(cx_b, {
4223 let project_b = project_b.clone();
4224 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
4225 move |_, cx| {
4226 cx.subscribe(&project_b, move |_, _, event, cx| {
4227 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
4228 disk_based_diagnostics_finished.store(true, SeqCst);
4229 for (buffer, _) in &guest_buffers {
4230 assert_eq!(
4231 buffer
4232 .read(cx)
4233 .snapshot()
4234 .diagnostics_in_range::<_, usize>(0..5, false)
4235 .count(),
4236 1,
4237 "expected a diagnostic for buffer {:?}",
4238 buffer.read(cx).file().unwrap().path(),
4239 );
4240 }
4241 }
4242 })
4243 .detach();
4244 }
4245 });
4246
4247 executor.run_until_parked();
4248 assert!(disk_based_diagnostics_finished.load(SeqCst));
4249}
4250
4251#[gpui::test(iterations = 10)]
4252async fn test_reloading_buffer_manually(
4253 executor: BackgroundExecutor,
4254 cx_a: &mut TestAppContext,
4255 cx_b: &mut TestAppContext,
4256) {
4257 let mut server = TestServer::start(executor.clone()).await;
4258 let client_a = server.create_client(cx_a, "user_a").await;
4259 let client_b = server.create_client(cx_b, "user_b").await;
4260 server
4261 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4262 .await;
4263 let active_call_a = cx_a.read(ActiveCall::global);
4264
4265 client_a
4266 .fs()
4267 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
4268 .await;
4269 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4270 let buffer_a = project_a
4271 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4272 .await
4273 .unwrap();
4274 let project_id = active_call_a
4275 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4276 .await
4277 .unwrap();
4278
4279 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4280
4281 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4282 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4283 buffer_b.update(cx_b, |buffer, cx| {
4284 buffer.edit([(4..7, "six")], None, cx);
4285 buffer.edit([(10..11, "6")], None, cx);
4286 assert_eq!(buffer.text(), "let six = 6;");
4287 assert!(buffer.is_dirty());
4288 assert!(!buffer.has_conflict());
4289 });
4290 executor.run_until_parked();
4291
4292 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4293
4294 client_a
4295 .fs()
4296 .save(
4297 "/a/a.rs".as_ref(),
4298 &Rope::from("let seven = 7;"),
4299 LineEnding::Unix,
4300 )
4301 .await
4302 .unwrap();
4303 executor.run_until_parked();
4304
4305 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4306
4307 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4308
4309 project_b
4310 .update(cx_b, |project, cx| {
4311 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4312 })
4313 .await
4314 .unwrap();
4315
4316 buffer_a.read_with(cx_a, |buffer, _| {
4317 assert_eq!(buffer.text(), "let seven = 7;");
4318 assert!(!buffer.is_dirty());
4319 assert!(!buffer.has_conflict());
4320 });
4321
4322 buffer_b.read_with(cx_b, |buffer, _| {
4323 assert_eq!(buffer.text(), "let seven = 7;");
4324 assert!(!buffer.is_dirty());
4325 assert!(!buffer.has_conflict());
4326 });
4327
4328 buffer_a.update(cx_a, |buffer, cx| {
4329 // Undoing on the host is a no-op when the reload was initiated by the guest.
4330 buffer.undo(cx);
4331 assert_eq!(buffer.text(), "let seven = 7;");
4332 assert!(!buffer.is_dirty());
4333 assert!(!buffer.has_conflict());
4334 });
4335 buffer_b.update(cx_b, |buffer, cx| {
4336 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4337 buffer.undo(cx);
4338 assert_eq!(buffer.text(), "let six = 6;");
4339 assert!(buffer.is_dirty());
4340 assert!(!buffer.has_conflict());
4341 });
4342}
4343
4344#[gpui::test(iterations = 10)]
4345async fn test_formatting_buffer(
4346 executor: BackgroundExecutor,
4347 cx_a: &mut TestAppContext,
4348 cx_b: &mut TestAppContext,
4349) {
4350 let mut server = TestServer::start(executor.clone()).await;
4351 let client_a = server.create_client(cx_a, "user_a").await;
4352 let client_b = server.create_client(cx_b, "user_b").await;
4353 server
4354 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4355 .await;
4356 let active_call_a = cx_a.read(ActiveCall::global);
4357
4358 client_a.language_registry().add(rust_lang());
4359 let mut fake_language_servers = client_a
4360 .language_registry()
4361 .register_fake_lsp("Rust", FakeLspAdapter::default());
4362
4363 // Here we insert a fake tree with a directory that exists on disk. This is needed
4364 // because later we'll invoke a command, which requires passing a working directory
4365 // that points to a valid location on disk.
4366 let directory = env::current_dir().unwrap();
4367 client_a
4368 .fs()
4369 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4370 .await;
4371 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4372 let project_id = active_call_a
4373 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4374 .await
4375 .unwrap();
4376 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4377 let lsp_store_b = project_b.update(cx_b, |p, _| p.lsp_store());
4378
4379 let buffer_b = project_b
4380 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4381 .await
4382 .unwrap();
4383
4384 let _handle = lsp_store_b.update(cx_b, |lsp_store, cx| {
4385 lsp_store.register_buffer_with_language_servers(&buffer_b, cx)
4386 });
4387 let fake_language_server = fake_language_servers.next().await.unwrap();
4388 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4389 Ok(Some(vec![
4390 lsp::TextEdit {
4391 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4392 new_text: "h".to_string(),
4393 },
4394 lsp::TextEdit {
4395 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4396 new_text: "y".to_string(),
4397 },
4398 ]))
4399 });
4400
4401 project_b
4402 .update(cx_b, |project, cx| {
4403 project.format(
4404 HashSet::from_iter([buffer_b.clone()]),
4405 true,
4406 FormatTrigger::Save,
4407 FormatTarget::Buffer,
4408 cx,
4409 )
4410 })
4411 .await
4412 .unwrap();
4413
4414 // The edits from the LSP are applied, and a final newline is added.
4415 assert_eq!(
4416 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4417 "let honey = \"two\"\n"
4418 );
4419
4420 // Ensure buffer can be formatted using an external command. Notice how the
4421 // host's configuration is honored as opposed to using the guest's settings.
4422 cx_a.update(|cx| {
4423 SettingsStore::update_global(cx, |store, cx| {
4424 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4425 file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
4426 vec![Formatter::External {
4427 command: "awk".into(),
4428 arguments: Some(vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into()),
4429 }]
4430 .into(),
4431 )));
4432 });
4433 });
4434 });
4435
4436 executor.allow_parking();
4437 project_b
4438 .update(cx_b, |project, cx| {
4439 project.format(
4440 HashSet::from_iter([buffer_b.clone()]),
4441 true,
4442 FormatTrigger::Save,
4443 FormatTarget::Buffer,
4444 cx,
4445 )
4446 })
4447 .await
4448 .unwrap();
4449 assert_eq!(
4450 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4451 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4452 );
4453}
4454
4455#[gpui::test(iterations = 10)]
4456async fn test_prettier_formatting_buffer(
4457 executor: BackgroundExecutor,
4458 cx_a: &mut TestAppContext,
4459 cx_b: &mut TestAppContext,
4460) {
4461 let mut server = TestServer::start(executor.clone()).await;
4462 let client_a = server.create_client(cx_a, "user_a").await;
4463 let client_b = server.create_client(cx_b, "user_b").await;
4464 server
4465 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4466 .await;
4467 let active_call_a = cx_a.read(ActiveCall::global);
4468
4469 let test_plugin = "test_plugin";
4470
4471 client_a.language_registry().add(Arc::new(Language::new(
4472 LanguageConfig {
4473 name: "TypeScript".into(),
4474 matcher: LanguageMatcher {
4475 path_suffixes: vec!["ts".to_string()],
4476 ..Default::default()
4477 },
4478 ..Default::default()
4479 },
4480 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
4481 )));
4482 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4483 "TypeScript",
4484 FakeLspAdapter {
4485 prettier_plugins: vec![test_plugin],
4486 ..Default::default()
4487 },
4488 );
4489
4490 // Here we insert a fake tree with a directory that exists on disk. This is needed
4491 // because later we'll invoke a command, which requires passing a working directory
4492 // that points to a valid location on disk.
4493 let directory = env::current_dir().unwrap();
4494 let buffer_text = "let one = \"two\"";
4495 client_a
4496 .fs()
4497 .insert_tree(&directory, json!({ "a.ts": buffer_text }))
4498 .await;
4499 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4500 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
4501 let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
4502 let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
4503
4504 let project_id = active_call_a
4505 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4506 .await
4507 .unwrap();
4508 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4509 let (buffer_b, _) = project_b
4510 .update(cx_b, |p, cx| {
4511 p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
4512 })
4513 .await
4514 .unwrap();
4515
4516 cx_a.update(|cx| {
4517 SettingsStore::update_global(cx, |store, cx| {
4518 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4519 file.defaults.formatter = Some(SelectedFormatter::Auto);
4520 file.defaults.prettier = Some(PrettierSettings {
4521 allowed: true,
4522 ..PrettierSettings::default()
4523 });
4524 });
4525 });
4526 });
4527 cx_b.update(|cx| {
4528 SettingsStore::update_global(cx, |store, cx| {
4529 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4530 file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
4531 vec![Formatter::LanguageServer { name: None }].into(),
4532 )));
4533 file.defaults.prettier = Some(PrettierSettings {
4534 allowed: true,
4535 ..PrettierSettings::default()
4536 });
4537 });
4538 });
4539 });
4540 let fake_language_server = fake_language_servers.next().await.unwrap();
4541 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4542 panic!(
4543 "Unexpected: prettier should be preferred since it's enabled and language supports it"
4544 )
4545 });
4546
4547 project_b
4548 .update(cx_b, |project, cx| {
4549 project.format(
4550 HashSet::from_iter([buffer_b.clone()]),
4551 true,
4552 FormatTrigger::Save,
4553 FormatTarget::Buffer,
4554 cx,
4555 )
4556 })
4557 .await
4558 .unwrap();
4559
4560 executor.run_until_parked();
4561 assert_eq!(
4562 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4563 buffer_text.to_string() + "\n" + prettier_format_suffix,
4564 "Prettier formatting was not applied to client buffer after client's request"
4565 );
4566
4567 project_a
4568 .update(cx_a, |project, cx| {
4569 project.format(
4570 HashSet::from_iter([buffer_a.clone()]),
4571 true,
4572 FormatTrigger::Manual,
4573 FormatTarget::Buffer,
4574 cx,
4575 )
4576 })
4577 .await
4578 .unwrap();
4579
4580 executor.run_until_parked();
4581 assert_eq!(
4582 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4583 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
4584 "Prettier formatting was not applied to client buffer after host's request"
4585 );
4586}
4587
4588#[gpui::test(iterations = 10)]
4589async fn test_definition(
4590 executor: BackgroundExecutor,
4591 cx_a: &mut TestAppContext,
4592 cx_b: &mut TestAppContext,
4593) {
4594 let mut server = TestServer::start(executor.clone()).await;
4595 let client_a = server.create_client(cx_a, "user_a").await;
4596 let client_b = server.create_client(cx_b, "user_b").await;
4597 server
4598 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4599 .await;
4600 let active_call_a = cx_a.read(ActiveCall::global);
4601
4602 let mut fake_language_servers = client_a
4603 .language_registry()
4604 .register_fake_lsp("Rust", Default::default());
4605 client_a.language_registry().add(rust_lang());
4606
4607 client_a
4608 .fs()
4609 .insert_tree(
4610 "/root",
4611 json!({
4612 "dir-1": {
4613 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4614 },
4615 "dir-2": {
4616 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4617 "c.rs": "type T2 = usize;",
4618 }
4619 }),
4620 )
4621 .await;
4622 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4623 let project_id = active_call_a
4624 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4625 .await
4626 .unwrap();
4627 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4628
4629 // Open the file on client B.
4630 let (buffer_b, _handle) = project_b
4631 .update(cx_b, |p, cx| {
4632 p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
4633 })
4634 .await
4635 .unwrap();
4636
4637 // Request the definition of a symbol as the guest.
4638 let fake_language_server = fake_language_servers.next().await.unwrap();
4639 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4640 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4641 lsp::Location::new(
4642 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4643 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4644 ),
4645 )))
4646 });
4647
4648 let definitions_1 = project_b
4649 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
4650 .await
4651 .unwrap();
4652 cx_b.read(|cx| {
4653 assert_eq!(definitions_1.len(), 1);
4654 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4655 let target_buffer = definitions_1[0].target.buffer.read(cx);
4656 assert_eq!(
4657 target_buffer.text(),
4658 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4659 );
4660 assert_eq!(
4661 definitions_1[0].target.range.to_point(target_buffer),
4662 Point::new(0, 6)..Point::new(0, 9)
4663 );
4664 });
4665
4666 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4667 // the previous call to `definition`.
4668 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4669 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4670 lsp::Location::new(
4671 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4672 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4673 ),
4674 )))
4675 });
4676
4677 let definitions_2 = project_b
4678 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4679 .await
4680 .unwrap();
4681 cx_b.read(|cx| {
4682 assert_eq!(definitions_2.len(), 1);
4683 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4684 let target_buffer = definitions_2[0].target.buffer.read(cx);
4685 assert_eq!(
4686 target_buffer.text(),
4687 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4688 );
4689 assert_eq!(
4690 definitions_2[0].target.range.to_point(target_buffer),
4691 Point::new(1, 6)..Point::new(1, 11)
4692 );
4693 });
4694 assert_eq!(
4695 definitions_1[0].target.buffer,
4696 definitions_2[0].target.buffer
4697 );
4698
4699 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4700 |req, _| async move {
4701 assert_eq!(
4702 req.text_document_position_params.position,
4703 lsp::Position::new(0, 7)
4704 );
4705 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4706 lsp::Location::new(
4707 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4708 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4709 ),
4710 )))
4711 },
4712 );
4713
4714 let type_definitions = project_b
4715 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4716 .await
4717 .unwrap();
4718 cx_b.read(|cx| {
4719 assert_eq!(type_definitions.len(), 1);
4720 let target_buffer = type_definitions[0].target.buffer.read(cx);
4721 assert_eq!(target_buffer.text(), "type T2 = usize;");
4722 assert_eq!(
4723 type_definitions[0].target.range.to_point(target_buffer),
4724 Point::new(0, 5)..Point::new(0, 7)
4725 );
4726 });
4727}
4728
4729#[gpui::test(iterations = 10)]
4730async fn test_references(
4731 executor: BackgroundExecutor,
4732 cx_a: &mut TestAppContext,
4733 cx_b: &mut TestAppContext,
4734) {
4735 let mut server = TestServer::start(executor.clone()).await;
4736 let client_a = server.create_client(cx_a, "user_a").await;
4737 let client_b = server.create_client(cx_b, "user_b").await;
4738 server
4739 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4740 .await;
4741 let active_call_a = cx_a.read(ActiveCall::global);
4742
4743 client_a.language_registry().add(rust_lang());
4744 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4745 "Rust",
4746 FakeLspAdapter {
4747 name: "my-fake-lsp-adapter",
4748 capabilities: lsp::ServerCapabilities {
4749 references_provider: Some(lsp::OneOf::Left(true)),
4750 ..Default::default()
4751 },
4752 ..Default::default()
4753 },
4754 );
4755
4756 client_a
4757 .fs()
4758 .insert_tree(
4759 "/root",
4760 json!({
4761 "dir-1": {
4762 "one.rs": "const ONE: usize = 1;",
4763 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4764 },
4765 "dir-2": {
4766 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4767 }
4768 }),
4769 )
4770 .await;
4771 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4772 let project_id = active_call_a
4773 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4774 .await
4775 .unwrap();
4776 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4777
4778 // Open the file on client B.
4779 let (buffer_b, _handle) = project_b
4780 .update(cx_b, |p, cx| {
4781 p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
4782 })
4783 .await
4784 .unwrap();
4785
4786 // Request references to a symbol as the guest.
4787 let fake_language_server = fake_language_servers.next().await.unwrap();
4788 let (lsp_response_tx, rx) = mpsc::unbounded::<Result<Option<Vec<lsp::Location>>>>();
4789 fake_language_server.handle_request::<lsp::request::References, _, _>({
4790 let rx = Arc::new(Mutex::new(Some(rx)));
4791 move |params, _| {
4792 assert_eq!(
4793 params.text_document_position.text_document.uri.as_str(),
4794 "file:///root/dir-1/one.rs"
4795 );
4796 let rx = rx.clone();
4797 async move {
4798 let mut response_rx = rx.lock().take().unwrap();
4799 let result = response_rx.next().await.unwrap();
4800 *rx.lock() = Some(response_rx);
4801 result
4802 }
4803 }
4804 });
4805
4806 let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx));
4807
4808 // User is informed that a request is pending.
4809 executor.run_until_parked();
4810 project_b.read_with(cx_b, |project, cx| {
4811 let status = project.language_server_statuses(cx).next().unwrap().1;
4812 assert_eq!(status.name, "my-fake-lsp-adapter");
4813 assert_eq!(
4814 status.pending_work.values().next().unwrap().message,
4815 Some("Finding references...".into())
4816 );
4817 });
4818
4819 // Cause the language server to respond.
4820 lsp_response_tx
4821 .unbounded_send(Ok(Some(vec![
4822 lsp::Location {
4823 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4824 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4825 },
4826 lsp::Location {
4827 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4828 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4829 },
4830 lsp::Location {
4831 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4832 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4833 },
4834 ])))
4835 .unwrap();
4836
4837 let references = references.await.unwrap();
4838 executor.run_until_parked();
4839 project_b.read_with(cx_b, |project, cx| {
4840 // User is informed that a request is no longer pending.
4841 let status = project.language_server_statuses(cx).next().unwrap().1;
4842 assert!(status.pending_work.is_empty());
4843
4844 assert_eq!(references.len(), 3);
4845 assert_eq!(project.worktrees(cx).count(), 2);
4846
4847 let two_buffer = references[0].buffer.read(cx);
4848 let three_buffer = references[2].buffer.read(cx);
4849 assert_eq!(
4850 two_buffer.file().unwrap().path().as_ref(),
4851 Path::new("two.rs")
4852 );
4853 assert_eq!(references[1].buffer, references[0].buffer);
4854 assert_eq!(
4855 three_buffer.file().unwrap().full_path(cx),
4856 Path::new("/root/dir-2/three.rs")
4857 );
4858
4859 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4860 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4861 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4862 });
4863
4864 let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx));
4865
4866 // User is informed that a request is pending.
4867 executor.run_until_parked();
4868 project_b.read_with(cx_b, |project, cx| {
4869 let status = project.language_server_statuses(cx).next().unwrap().1;
4870 assert_eq!(status.name, "my-fake-lsp-adapter");
4871 assert_eq!(
4872 status.pending_work.values().next().unwrap().message,
4873 Some("Finding references...".into())
4874 );
4875 });
4876
4877 // Cause the LSP request to fail.
4878 lsp_response_tx
4879 .unbounded_send(Err(anyhow!("can't find references")))
4880 .unwrap();
4881 references.await.unwrap_err();
4882
4883 // User is informed that the request is no longer pending.
4884 executor.run_until_parked();
4885 project_b.read_with(cx_b, |project, cx| {
4886 let status = project.language_server_statuses(cx).next().unwrap().1;
4887 assert!(status.pending_work.is_empty());
4888 });
4889}
4890
4891#[gpui::test(iterations = 10)]
4892async fn test_project_search(
4893 executor: BackgroundExecutor,
4894 cx_a: &mut TestAppContext,
4895 cx_b: &mut TestAppContext,
4896) {
4897 let mut server = TestServer::start(executor.clone()).await;
4898 let client_a = server.create_client(cx_a, "user_a").await;
4899 let client_b = server.create_client(cx_b, "user_b").await;
4900 server
4901 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4902 .await;
4903 let active_call_a = cx_a.read(ActiveCall::global);
4904
4905 client_a
4906 .fs()
4907 .insert_tree(
4908 "/root",
4909 json!({
4910 "dir-1": {
4911 "a": "hello world",
4912 "b": "goodnight moon",
4913 "c": "a world of goo",
4914 "d": "world champion of clown world",
4915 },
4916 "dir-2": {
4917 "e": "disney world is fun",
4918 }
4919 }),
4920 )
4921 .await;
4922 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4923 let (worktree_2, _) = project_a
4924 .update(cx_a, |p, cx| {
4925 p.find_or_create_worktree("/root/dir-2", true, cx)
4926 })
4927 .await
4928 .unwrap();
4929 worktree_2
4930 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4931 .await;
4932 let project_id = active_call_a
4933 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4934 .await
4935 .unwrap();
4936
4937 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4938
4939 // Perform a search as the guest.
4940 let mut results = HashMap::default();
4941 let mut search_rx = project_b.update(cx_b, |project, cx| {
4942 project.search(
4943 SearchQuery::text(
4944 "world",
4945 false,
4946 false,
4947 false,
4948 Default::default(),
4949 Default::default(),
4950 None,
4951 )
4952 .unwrap(),
4953 cx,
4954 )
4955 });
4956 while let Some(result) = search_rx.next().await {
4957 match result {
4958 SearchResult::Buffer { buffer, ranges } => {
4959 results.entry(buffer).or_insert(ranges);
4960 }
4961 SearchResult::LimitReached => {
4962 panic!("Unexpectedly reached search limit in tests. If you do want to assert limit-reached, change this panic call.")
4963 }
4964 };
4965 }
4966
4967 let mut ranges_by_path = results
4968 .into_iter()
4969 .map(|(buffer, ranges)| {
4970 buffer.read_with(cx_b, |buffer, cx| {
4971 let path = buffer.file().unwrap().full_path(cx);
4972 let offset_ranges = ranges
4973 .into_iter()
4974 .map(|range| range.to_offset(buffer))
4975 .collect::<Vec<_>>();
4976 (path, offset_ranges)
4977 })
4978 })
4979 .collect::<Vec<_>>();
4980 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4981
4982 assert_eq!(
4983 ranges_by_path,
4984 &[
4985 (PathBuf::from("dir-1/a"), vec![6..11]),
4986 (PathBuf::from("dir-1/c"), vec![2..7]),
4987 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4988 (PathBuf::from("dir-2/e"), vec![7..12]),
4989 ]
4990 );
4991}
4992
4993#[gpui::test(iterations = 10)]
4994async fn test_document_highlights(
4995 executor: BackgroundExecutor,
4996 cx_a: &mut TestAppContext,
4997 cx_b: &mut TestAppContext,
4998) {
4999 let mut server = TestServer::start(executor.clone()).await;
5000 let client_a = server.create_client(cx_a, "user_a").await;
5001 let client_b = server.create_client(cx_b, "user_b").await;
5002 server
5003 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5004 .await;
5005 let active_call_a = cx_a.read(ActiveCall::global);
5006
5007 client_a
5008 .fs()
5009 .insert_tree(
5010 "/root-1",
5011 json!({
5012 "main.rs": "fn double(number: i32) -> i32 { number + number }",
5013 }),
5014 )
5015 .await;
5016
5017 let mut fake_language_servers = client_a
5018 .language_registry()
5019 .register_fake_lsp("Rust", Default::default());
5020 client_a.language_registry().add(rust_lang());
5021
5022 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
5023 let project_id = active_call_a
5024 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5025 .await
5026 .unwrap();
5027 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5028
5029 // Open the file on client B.
5030 let (buffer_b, _handle) = project_b
5031 .update(cx_b, |p, cx| {
5032 p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
5033 })
5034 .await
5035 .unwrap();
5036
5037 // Request document highlights as the guest.
5038 let fake_language_server = fake_language_servers.next().await.unwrap();
5039 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
5040 |params, _| async move {
5041 assert_eq!(
5042 params
5043 .text_document_position_params
5044 .text_document
5045 .uri
5046 .as_str(),
5047 "file:///root-1/main.rs"
5048 );
5049 assert_eq!(
5050 params.text_document_position_params.position,
5051 lsp::Position::new(0, 34)
5052 );
5053 Ok(Some(vec![
5054 lsp::DocumentHighlight {
5055 kind: Some(lsp::DocumentHighlightKind::WRITE),
5056 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
5057 },
5058 lsp::DocumentHighlight {
5059 kind: Some(lsp::DocumentHighlightKind::READ),
5060 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
5061 },
5062 lsp::DocumentHighlight {
5063 kind: Some(lsp::DocumentHighlightKind::READ),
5064 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
5065 },
5066 ]))
5067 },
5068 );
5069
5070 let highlights = project_b
5071 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
5072 .await
5073 .unwrap();
5074
5075 buffer_b.read_with(cx_b, |buffer, _| {
5076 let snapshot = buffer.snapshot();
5077
5078 let highlights = highlights
5079 .into_iter()
5080 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
5081 .collect::<Vec<_>>();
5082 assert_eq!(
5083 highlights,
5084 &[
5085 (lsp::DocumentHighlightKind::WRITE, 10..16),
5086 (lsp::DocumentHighlightKind::READ, 32..38),
5087 (lsp::DocumentHighlightKind::READ, 41..47)
5088 ]
5089 )
5090 });
5091}
5092
5093#[gpui::test(iterations = 10)]
5094async fn test_lsp_hover(
5095 executor: BackgroundExecutor,
5096 cx_a: &mut TestAppContext,
5097 cx_b: &mut TestAppContext,
5098) {
5099 let mut server = TestServer::start(executor.clone()).await;
5100 let client_a = server.create_client(cx_a, "user_a").await;
5101 let client_b = server.create_client(cx_b, "user_b").await;
5102 server
5103 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5104 .await;
5105 let active_call_a = cx_a.read(ActiveCall::global);
5106
5107 client_a
5108 .fs()
5109 .insert_tree(
5110 "/root-1",
5111 json!({
5112 "main.rs": "use std::collections::HashMap;",
5113 }),
5114 )
5115 .await;
5116
5117 client_a.language_registry().add(rust_lang());
5118 let language_server_names = ["rust-analyzer", "CrabLang-ls"];
5119 let mut language_servers = [
5120 client_a.language_registry().register_fake_lsp(
5121 "Rust",
5122 FakeLspAdapter {
5123 name: "rust-analyzer",
5124 capabilities: lsp::ServerCapabilities {
5125 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5126 ..lsp::ServerCapabilities::default()
5127 },
5128 ..FakeLspAdapter::default()
5129 },
5130 ),
5131 client_a.language_registry().register_fake_lsp(
5132 "Rust",
5133 FakeLspAdapter {
5134 name: "CrabLang-ls",
5135 capabilities: lsp::ServerCapabilities {
5136 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5137 ..lsp::ServerCapabilities::default()
5138 },
5139 ..FakeLspAdapter::default()
5140 },
5141 ),
5142 ];
5143
5144 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
5145 let project_id = active_call_a
5146 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5147 .await
5148 .unwrap();
5149 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5150
5151 // Open the file as the guest
5152 let (buffer_b, _handle) = project_b
5153 .update(cx_b, |p, cx| {
5154 p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
5155 })
5156 .await
5157 .unwrap();
5158
5159 let mut servers_with_hover_requests = HashMap::default();
5160 for i in 0..language_server_names.len() {
5161 let new_server = language_servers[i].next().await.unwrap_or_else(|| {
5162 panic!(
5163 "Failed to get language server #{i} with name {}",
5164 &language_server_names[i]
5165 )
5166 });
5167 let new_server_name = new_server.server.name();
5168 assert!(
5169 !servers_with_hover_requests.contains_key(&new_server_name),
5170 "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
5171 );
5172 match new_server_name.as_ref() {
5173 "CrabLang-ls" => {
5174 servers_with_hover_requests.insert(
5175 new_server_name.clone(),
5176 new_server.handle_request::<lsp::request::HoverRequest, _, _>(
5177 move |params, _| {
5178 assert_eq!(
5179 params
5180 .text_document_position_params
5181 .text_document
5182 .uri
5183 .as_str(),
5184 "file:///root-1/main.rs"
5185 );
5186 let name = new_server_name.clone();
5187 async move {
5188 Ok(Some(lsp::Hover {
5189 contents: lsp::HoverContents::Scalar(
5190 lsp::MarkedString::String(format!("{name} hover")),
5191 ),
5192 range: None,
5193 }))
5194 }
5195 },
5196 ),
5197 );
5198 }
5199 "rust-analyzer" => {
5200 servers_with_hover_requests.insert(
5201 new_server_name.clone(),
5202 new_server.handle_request::<lsp::request::HoverRequest, _, _>(
5203 |params, _| async move {
5204 assert_eq!(
5205 params
5206 .text_document_position_params
5207 .text_document
5208 .uri
5209 .as_str(),
5210 "file:///root-1/main.rs"
5211 );
5212 assert_eq!(
5213 params.text_document_position_params.position,
5214 lsp::Position::new(0, 22)
5215 );
5216 Ok(Some(lsp::Hover {
5217 contents: lsp::HoverContents::Array(vec![
5218 lsp::MarkedString::String("Test hover content.".to_string()),
5219 lsp::MarkedString::LanguageString(lsp::LanguageString {
5220 language: "Rust".to_string(),
5221 value: "let foo = 42;".to_string(),
5222 }),
5223 ]),
5224 range: Some(lsp::Range::new(
5225 lsp::Position::new(0, 22),
5226 lsp::Position::new(0, 29),
5227 )),
5228 }))
5229 },
5230 ),
5231 );
5232 }
5233 unexpected => panic!("Unexpected server name: {unexpected}"),
5234 }
5235 }
5236
5237 // Request hover information as the guest.
5238 let mut hovers = project_b
5239 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
5240 .await;
5241 assert_eq!(
5242 hovers.len(),
5243 2,
5244 "Expected two hovers from both language servers, but got: {hovers:?}"
5245 );
5246
5247 let _: Vec<()> = futures::future::join_all(servers_with_hover_requests.into_values().map(
5248 |mut hover_request| async move {
5249 hover_request
5250 .next()
5251 .await
5252 .expect("All hover requests should have been triggered")
5253 },
5254 ))
5255 .await;
5256
5257 hovers.sort_by_key(|hover| hover.contents.len());
5258 let first_hover = hovers.first().cloned().unwrap();
5259 assert_eq!(
5260 first_hover.contents,
5261 vec![project::HoverBlock {
5262 text: "CrabLang-ls hover".to_string(),
5263 kind: HoverBlockKind::Markdown,
5264 },]
5265 );
5266 let second_hover = hovers.last().cloned().unwrap();
5267 assert_eq!(
5268 second_hover.contents,
5269 vec![
5270 project::HoverBlock {
5271 text: "Test hover content.".to_string(),
5272 kind: HoverBlockKind::Markdown,
5273 },
5274 project::HoverBlock {
5275 text: "let foo = 42;".to_string(),
5276 kind: HoverBlockKind::Code {
5277 language: "Rust".to_string()
5278 },
5279 }
5280 ]
5281 );
5282 buffer_b.read_with(cx_b, |buffer, _| {
5283 let snapshot = buffer.snapshot();
5284 assert_eq!(second_hover.range.unwrap().to_offset(&snapshot), 22..29);
5285 });
5286}
5287
5288#[gpui::test(iterations = 10)]
5289async fn test_project_symbols(
5290 executor: BackgroundExecutor,
5291 cx_a: &mut TestAppContext,
5292 cx_b: &mut TestAppContext,
5293) {
5294 let mut server = TestServer::start(executor.clone()).await;
5295 let client_a = server.create_client(cx_a, "user_a").await;
5296 let client_b = server.create_client(cx_b, "user_b").await;
5297 server
5298 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5299 .await;
5300 let active_call_a = cx_a.read(ActiveCall::global);
5301
5302 client_a.language_registry().add(rust_lang());
5303 let mut fake_language_servers = client_a
5304 .language_registry()
5305 .register_fake_lsp("Rust", Default::default());
5306
5307 client_a
5308 .fs()
5309 .insert_tree(
5310 "/code",
5311 json!({
5312 "crate-1": {
5313 "one.rs": "const ONE: usize = 1;",
5314 },
5315 "crate-2": {
5316 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
5317 },
5318 "private": {
5319 "passwords.txt": "the-password",
5320 }
5321 }),
5322 )
5323 .await;
5324 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
5325 let project_id = active_call_a
5326 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5327 .await
5328 .unwrap();
5329 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5330
5331 // Cause the language server to start.
5332 let _buffer = project_b
5333 .update(cx_b, |p, cx| {
5334 p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
5335 })
5336 .await
5337 .unwrap();
5338
5339 let fake_language_server = fake_language_servers.next().await.unwrap();
5340 fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
5341 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
5342 #[allow(deprecated)]
5343 lsp::SymbolInformation {
5344 name: "TWO".into(),
5345 location: lsp::Location {
5346 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
5347 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5348 },
5349 kind: lsp::SymbolKind::CONSTANT,
5350 tags: None,
5351 container_name: None,
5352 deprecated: None,
5353 },
5354 ])))
5355 });
5356
5357 // Request the definition of a symbol as the guest.
5358 let symbols = project_b
5359 .update(cx_b, |p, cx| p.symbols("two", cx))
5360 .await
5361 .unwrap();
5362 assert_eq!(symbols.len(), 1);
5363 assert_eq!(symbols[0].name, "TWO");
5364
5365 // Open one of the returned symbols.
5366 let buffer_b_2 = project_b
5367 .update(cx_b, |project, cx| {
5368 project.open_buffer_for_symbol(&symbols[0], cx)
5369 })
5370 .await
5371 .unwrap();
5372
5373 buffer_b_2.read_with(cx_b, |buffer, cx| {
5374 assert_eq!(
5375 buffer.file().unwrap().full_path(cx),
5376 Path::new("/code/crate-2/two.rs")
5377 );
5378 });
5379
5380 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
5381 let mut fake_symbol = symbols[0].clone();
5382 fake_symbol.path.path = Path::new("/code/secrets").into();
5383 let error = project_b
5384 .update(cx_b, |project, cx| {
5385 project.open_buffer_for_symbol(&fake_symbol, cx)
5386 })
5387 .await
5388 .unwrap_err();
5389 assert!(error.to_string().contains("invalid symbol signature"));
5390}
5391
5392#[gpui::test(iterations = 10)]
5393async fn test_open_buffer_while_getting_definition_pointing_to_it(
5394 executor: BackgroundExecutor,
5395 cx_a: &mut TestAppContext,
5396 cx_b: &mut TestAppContext,
5397 mut rng: StdRng,
5398) {
5399 let mut server = TestServer::start(executor.clone()).await;
5400 let client_a = server.create_client(cx_a, "user_a").await;
5401 let client_b = server.create_client(cx_b, "user_b").await;
5402 server
5403 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5404 .await;
5405 let active_call_a = cx_a.read(ActiveCall::global);
5406
5407 client_a.language_registry().add(rust_lang());
5408 let mut fake_language_servers = client_a
5409 .language_registry()
5410 .register_fake_lsp("Rust", Default::default());
5411
5412 client_a
5413 .fs()
5414 .insert_tree(
5415 "/root",
5416 json!({
5417 "a.rs": "const ONE: usize = b::TWO;",
5418 "b.rs": "const TWO: usize = 2",
5419 }),
5420 )
5421 .await;
5422 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
5423 let project_id = active_call_a
5424 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5425 .await
5426 .unwrap();
5427 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5428
5429 let (buffer_b1, _lsp) = project_b
5430 .update(cx_b, |p, cx| {
5431 p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
5432 })
5433 .await
5434 .unwrap();
5435
5436 let fake_language_server = fake_language_servers.next().await.unwrap();
5437 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
5438 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
5439 lsp::Location::new(
5440 lsp::Url::from_file_path("/root/b.rs").unwrap(),
5441 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5442 ),
5443 )))
5444 });
5445
5446 let definitions;
5447 let buffer_b2;
5448 if rng.gen() {
5449 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5450 (buffer_b2, _) = project_b
5451 .update(cx_b, |p, cx| {
5452 p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
5453 })
5454 .await
5455 .unwrap();
5456 } else {
5457 (buffer_b2, _) = project_b
5458 .update(cx_b, |p, cx| {
5459 p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
5460 })
5461 .await
5462 .unwrap();
5463 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5464 }
5465
5466 let definitions = definitions.await.unwrap();
5467 assert_eq!(definitions.len(), 1);
5468 assert_eq!(definitions[0].target.buffer, buffer_b2);
5469}
5470
5471#[gpui::test(iterations = 10)]
5472async fn test_contacts(
5473 executor: BackgroundExecutor,
5474 cx_a: &mut TestAppContext,
5475 cx_b: &mut TestAppContext,
5476 cx_c: &mut TestAppContext,
5477 cx_d: &mut TestAppContext,
5478) {
5479 let mut server = TestServer::start(executor.clone()).await;
5480 let client_a = server.create_client(cx_a, "user_a").await;
5481 let client_b = server.create_client(cx_b, "user_b").await;
5482 let client_c = server.create_client(cx_c, "user_c").await;
5483 let client_d = server.create_client(cx_d, "user_d").await;
5484 server
5485 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5486 .await;
5487 let active_call_a = cx_a.read(ActiveCall::global);
5488 let active_call_b = cx_b.read(ActiveCall::global);
5489 let active_call_c = cx_c.read(ActiveCall::global);
5490 let _active_call_d = cx_d.read(ActiveCall::global);
5491
5492 executor.run_until_parked();
5493 assert_eq!(
5494 contacts(&client_a, cx_a),
5495 [
5496 ("user_b".to_string(), "online", "free"),
5497 ("user_c".to_string(), "online", "free")
5498 ]
5499 );
5500 assert_eq!(
5501 contacts(&client_b, cx_b),
5502 [
5503 ("user_a".to_string(), "online", "free"),
5504 ("user_c".to_string(), "online", "free")
5505 ]
5506 );
5507 assert_eq!(
5508 contacts(&client_c, cx_c),
5509 [
5510 ("user_a".to_string(), "online", "free"),
5511 ("user_b".to_string(), "online", "free")
5512 ]
5513 );
5514 assert_eq!(contacts(&client_d, cx_d), []);
5515
5516 server.disconnect_client(client_c.peer_id().unwrap());
5517 server.forbid_connections();
5518 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5519 assert_eq!(
5520 contacts(&client_a, cx_a),
5521 [
5522 ("user_b".to_string(), "online", "free"),
5523 ("user_c".to_string(), "offline", "free")
5524 ]
5525 );
5526 assert_eq!(
5527 contacts(&client_b, cx_b),
5528 [
5529 ("user_a".to_string(), "online", "free"),
5530 ("user_c".to_string(), "offline", "free")
5531 ]
5532 );
5533 assert_eq!(contacts(&client_c, cx_c), []);
5534 assert_eq!(contacts(&client_d, cx_d), []);
5535
5536 server.allow_connections();
5537 client_c
5538 .authenticate_and_connect(false, &cx_c.to_async())
5539 .await
5540 .unwrap();
5541
5542 executor.run_until_parked();
5543 assert_eq!(
5544 contacts(&client_a, cx_a),
5545 [
5546 ("user_b".to_string(), "online", "free"),
5547 ("user_c".to_string(), "online", "free")
5548 ]
5549 );
5550 assert_eq!(
5551 contacts(&client_b, cx_b),
5552 [
5553 ("user_a".to_string(), "online", "free"),
5554 ("user_c".to_string(), "online", "free")
5555 ]
5556 );
5557 assert_eq!(
5558 contacts(&client_c, cx_c),
5559 [
5560 ("user_a".to_string(), "online", "free"),
5561 ("user_b".to_string(), "online", "free")
5562 ]
5563 );
5564 assert_eq!(contacts(&client_d, cx_d), []);
5565
5566 active_call_a
5567 .update(cx_a, |call, cx| {
5568 call.invite(client_b.user_id().unwrap(), None, cx)
5569 })
5570 .await
5571 .unwrap();
5572 executor.run_until_parked();
5573 assert_eq!(
5574 contacts(&client_a, cx_a),
5575 [
5576 ("user_b".to_string(), "online", "busy"),
5577 ("user_c".to_string(), "online", "free")
5578 ]
5579 );
5580 assert_eq!(
5581 contacts(&client_b, cx_b),
5582 [
5583 ("user_a".to_string(), "online", "busy"),
5584 ("user_c".to_string(), "online", "free")
5585 ]
5586 );
5587 assert_eq!(
5588 contacts(&client_c, cx_c),
5589 [
5590 ("user_a".to_string(), "online", "busy"),
5591 ("user_b".to_string(), "online", "busy")
5592 ]
5593 );
5594 assert_eq!(contacts(&client_d, cx_d), []);
5595
5596 // Client B and client D become contacts while client B is being called.
5597 server
5598 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5599 .await;
5600 executor.run_until_parked();
5601 assert_eq!(
5602 contacts(&client_a, cx_a),
5603 [
5604 ("user_b".to_string(), "online", "busy"),
5605 ("user_c".to_string(), "online", "free")
5606 ]
5607 );
5608 assert_eq!(
5609 contacts(&client_b, cx_b),
5610 [
5611 ("user_a".to_string(), "online", "busy"),
5612 ("user_c".to_string(), "online", "free"),
5613 ("user_d".to_string(), "online", "free"),
5614 ]
5615 );
5616 assert_eq!(
5617 contacts(&client_c, cx_c),
5618 [
5619 ("user_a".to_string(), "online", "busy"),
5620 ("user_b".to_string(), "online", "busy")
5621 ]
5622 );
5623 assert_eq!(
5624 contacts(&client_d, cx_d),
5625 [("user_b".to_string(), "online", "busy")]
5626 );
5627
5628 active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
5629 executor.run_until_parked();
5630 assert_eq!(
5631 contacts(&client_a, cx_a),
5632 [
5633 ("user_b".to_string(), "online", "free"),
5634 ("user_c".to_string(), "online", "free")
5635 ]
5636 );
5637 assert_eq!(
5638 contacts(&client_b, cx_b),
5639 [
5640 ("user_a".to_string(), "online", "free"),
5641 ("user_c".to_string(), "online", "free"),
5642 ("user_d".to_string(), "online", "free")
5643 ]
5644 );
5645 assert_eq!(
5646 contacts(&client_c, cx_c),
5647 [
5648 ("user_a".to_string(), "online", "free"),
5649 ("user_b".to_string(), "online", "free")
5650 ]
5651 );
5652 assert_eq!(
5653 contacts(&client_d, cx_d),
5654 [("user_b".to_string(), "online", "free")]
5655 );
5656
5657 active_call_c
5658 .update(cx_c, |call, cx| {
5659 call.invite(client_a.user_id().unwrap(), None, cx)
5660 })
5661 .await
5662 .unwrap();
5663 executor.run_until_parked();
5664 assert_eq!(
5665 contacts(&client_a, cx_a),
5666 [
5667 ("user_b".to_string(), "online", "free"),
5668 ("user_c".to_string(), "online", "busy")
5669 ]
5670 );
5671 assert_eq!(
5672 contacts(&client_b, cx_b),
5673 [
5674 ("user_a".to_string(), "online", "busy"),
5675 ("user_c".to_string(), "online", "busy"),
5676 ("user_d".to_string(), "online", "free")
5677 ]
5678 );
5679 assert_eq!(
5680 contacts(&client_c, cx_c),
5681 [
5682 ("user_a".to_string(), "online", "busy"),
5683 ("user_b".to_string(), "online", "free")
5684 ]
5685 );
5686 assert_eq!(
5687 contacts(&client_d, cx_d),
5688 [("user_b".to_string(), "online", "free")]
5689 );
5690
5691 active_call_a
5692 .update(cx_a, |call, cx| call.accept_incoming(cx))
5693 .await
5694 .unwrap();
5695 executor.run_until_parked();
5696 assert_eq!(
5697 contacts(&client_a, cx_a),
5698 [
5699 ("user_b".to_string(), "online", "free"),
5700 ("user_c".to_string(), "online", "busy")
5701 ]
5702 );
5703 assert_eq!(
5704 contacts(&client_b, cx_b),
5705 [
5706 ("user_a".to_string(), "online", "busy"),
5707 ("user_c".to_string(), "online", "busy"),
5708 ("user_d".to_string(), "online", "free")
5709 ]
5710 );
5711 assert_eq!(
5712 contacts(&client_c, cx_c),
5713 [
5714 ("user_a".to_string(), "online", "busy"),
5715 ("user_b".to_string(), "online", "free")
5716 ]
5717 );
5718 assert_eq!(
5719 contacts(&client_d, cx_d),
5720 [("user_b".to_string(), "online", "free")]
5721 );
5722
5723 active_call_a
5724 .update(cx_a, |call, cx| {
5725 call.invite(client_b.user_id().unwrap(), None, cx)
5726 })
5727 .await
5728 .unwrap();
5729 executor.run_until_parked();
5730 assert_eq!(
5731 contacts(&client_a, cx_a),
5732 [
5733 ("user_b".to_string(), "online", "busy"),
5734 ("user_c".to_string(), "online", "busy")
5735 ]
5736 );
5737 assert_eq!(
5738 contacts(&client_b, cx_b),
5739 [
5740 ("user_a".to_string(), "online", "busy"),
5741 ("user_c".to_string(), "online", "busy"),
5742 ("user_d".to_string(), "online", "free")
5743 ]
5744 );
5745 assert_eq!(
5746 contacts(&client_c, cx_c),
5747 [
5748 ("user_a".to_string(), "online", "busy"),
5749 ("user_b".to_string(), "online", "busy")
5750 ]
5751 );
5752 assert_eq!(
5753 contacts(&client_d, cx_d),
5754 [("user_b".to_string(), "online", "busy")]
5755 );
5756
5757 active_call_a
5758 .update(cx_a, |call, cx| call.hang_up(cx))
5759 .await
5760 .unwrap();
5761 executor.run_until_parked();
5762 assert_eq!(
5763 contacts(&client_a, cx_a),
5764 [
5765 ("user_b".to_string(), "online", "free"),
5766 ("user_c".to_string(), "online", "free")
5767 ]
5768 );
5769 assert_eq!(
5770 contacts(&client_b, cx_b),
5771 [
5772 ("user_a".to_string(), "online", "free"),
5773 ("user_c".to_string(), "online", "free"),
5774 ("user_d".to_string(), "online", "free")
5775 ]
5776 );
5777 assert_eq!(
5778 contacts(&client_c, cx_c),
5779 [
5780 ("user_a".to_string(), "online", "free"),
5781 ("user_b".to_string(), "online", "free")
5782 ]
5783 );
5784 assert_eq!(
5785 contacts(&client_d, cx_d),
5786 [("user_b".to_string(), "online", "free")]
5787 );
5788
5789 active_call_a
5790 .update(cx_a, |call, cx| {
5791 call.invite(client_b.user_id().unwrap(), None, cx)
5792 })
5793 .await
5794 .unwrap();
5795 executor.run_until_parked();
5796 assert_eq!(
5797 contacts(&client_a, cx_a),
5798 [
5799 ("user_b".to_string(), "online", "busy"),
5800 ("user_c".to_string(), "online", "free")
5801 ]
5802 );
5803 assert_eq!(
5804 contacts(&client_b, cx_b),
5805 [
5806 ("user_a".to_string(), "online", "busy"),
5807 ("user_c".to_string(), "online", "free"),
5808 ("user_d".to_string(), "online", "free")
5809 ]
5810 );
5811 assert_eq!(
5812 contacts(&client_c, cx_c),
5813 [
5814 ("user_a".to_string(), "online", "busy"),
5815 ("user_b".to_string(), "online", "busy")
5816 ]
5817 );
5818 assert_eq!(
5819 contacts(&client_d, cx_d),
5820 [("user_b".to_string(), "online", "busy")]
5821 );
5822
5823 server.forbid_connections();
5824 server.disconnect_client(client_a.peer_id().unwrap());
5825 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5826 assert_eq!(contacts(&client_a, cx_a), []);
5827 assert_eq!(
5828 contacts(&client_b, cx_b),
5829 [
5830 ("user_a".to_string(), "offline", "free"),
5831 ("user_c".to_string(), "online", "free"),
5832 ("user_d".to_string(), "online", "free")
5833 ]
5834 );
5835 assert_eq!(
5836 contacts(&client_c, cx_c),
5837 [
5838 ("user_a".to_string(), "offline", "free"),
5839 ("user_b".to_string(), "online", "free")
5840 ]
5841 );
5842 assert_eq!(
5843 contacts(&client_d, cx_d),
5844 [("user_b".to_string(), "online", "free")]
5845 );
5846
5847 // Test removing a contact
5848 client_b
5849 .user_store()
5850 .update(cx_b, |store, cx| {
5851 store.remove_contact(client_c.user_id().unwrap(), cx)
5852 })
5853 .await
5854 .unwrap();
5855 executor.run_until_parked();
5856 assert_eq!(
5857 contacts(&client_b, cx_b),
5858 [
5859 ("user_a".to_string(), "offline", "free"),
5860 ("user_d".to_string(), "online", "free")
5861 ]
5862 );
5863 assert_eq!(
5864 contacts(&client_c, cx_c),
5865 [("user_a".to_string(), "offline", "free"),]
5866 );
5867
5868 fn contacts(
5869 client: &TestClient,
5870 cx: &TestAppContext,
5871 ) -> Vec<(String, &'static str, &'static str)> {
5872 client.user_store().read_with(cx, |store, _| {
5873 store
5874 .contacts()
5875 .iter()
5876 .map(|contact| {
5877 (
5878 contact.user.github_login.clone(),
5879 if contact.online { "online" } else { "offline" },
5880 if contact.busy { "busy" } else { "free" },
5881 )
5882 })
5883 .collect()
5884 })
5885 }
5886}
5887
5888#[gpui::test(iterations = 10)]
5889async fn test_contact_requests(
5890 executor: BackgroundExecutor,
5891 cx_a: &mut TestAppContext,
5892 cx_a2: &mut TestAppContext,
5893 cx_b: &mut TestAppContext,
5894 cx_b2: &mut TestAppContext,
5895 cx_c: &mut TestAppContext,
5896 cx_c2: &mut TestAppContext,
5897) {
5898 // Connect to a server as 3 clients.
5899 let mut server = TestServer::start(executor.clone()).await;
5900 let client_a = server.create_client(cx_a, "user_a").await;
5901 let client_a2 = server.create_client(cx_a2, "user_a").await;
5902 let client_b = server.create_client(cx_b, "user_b").await;
5903 let client_b2 = server.create_client(cx_b2, "user_b").await;
5904 let client_c = server.create_client(cx_c, "user_c").await;
5905 let client_c2 = server.create_client(cx_c2, "user_c").await;
5906
5907 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
5908 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
5909 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
5910
5911 // User A and User C request that user B become their contact.
5912 client_a
5913 .user_store()
5914 .update(cx_a, |store, cx| {
5915 store.request_contact(client_b.user_id().unwrap(), cx)
5916 })
5917 .await
5918 .unwrap();
5919 client_c
5920 .user_store()
5921 .update(cx_c, |store, cx| {
5922 store.request_contact(client_b.user_id().unwrap(), cx)
5923 })
5924 .await
5925 .unwrap();
5926 executor.run_until_parked();
5927
5928 // All users see the pending request appear in all their clients.
5929 assert_eq!(
5930 client_a.summarize_contacts(cx_a).outgoing_requests,
5931 &["user_b"]
5932 );
5933 assert_eq!(
5934 client_a2.summarize_contacts(cx_a2).outgoing_requests,
5935 &["user_b"]
5936 );
5937 assert_eq!(
5938 client_b.summarize_contacts(cx_b).incoming_requests,
5939 &["user_a", "user_c"]
5940 );
5941 assert_eq!(
5942 client_b2.summarize_contacts(cx_b2).incoming_requests,
5943 &["user_a", "user_c"]
5944 );
5945 assert_eq!(
5946 client_c.summarize_contacts(cx_c).outgoing_requests,
5947 &["user_b"]
5948 );
5949 assert_eq!(
5950 client_c2.summarize_contacts(cx_c2).outgoing_requests,
5951 &["user_b"]
5952 );
5953
5954 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
5955 disconnect_and_reconnect(&client_a, cx_a).await;
5956 disconnect_and_reconnect(&client_b, cx_b).await;
5957 disconnect_and_reconnect(&client_c, cx_c).await;
5958 executor.run_until_parked();
5959 assert_eq!(
5960 client_a.summarize_contacts(cx_a).outgoing_requests,
5961 &["user_b"]
5962 );
5963 assert_eq!(
5964 client_b.summarize_contacts(cx_b).incoming_requests,
5965 &["user_a", "user_c"]
5966 );
5967 assert_eq!(
5968 client_c.summarize_contacts(cx_c).outgoing_requests,
5969 &["user_b"]
5970 );
5971
5972 // User B accepts the request from user A.
5973 client_b
5974 .user_store()
5975 .update(cx_b, |store, cx| {
5976 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5977 })
5978 .await
5979 .unwrap();
5980
5981 executor.run_until_parked();
5982
5983 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
5984 let contacts_b = client_b.summarize_contacts(cx_b);
5985 assert_eq!(contacts_b.current, &["user_a"]);
5986 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
5987 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5988 assert_eq!(contacts_b2.current, &["user_a"]);
5989 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
5990
5991 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
5992 let contacts_a = client_a.summarize_contacts(cx_a);
5993 assert_eq!(contacts_a.current, &["user_b"]);
5994 assert!(contacts_a.outgoing_requests.is_empty());
5995 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
5996 assert_eq!(contacts_a2.current, &["user_b"]);
5997 assert!(contacts_a2.outgoing_requests.is_empty());
5998
5999 // Contacts are present upon connecting (tested here via disconnect/reconnect)
6000 disconnect_and_reconnect(&client_a, cx_a).await;
6001 disconnect_and_reconnect(&client_b, cx_b).await;
6002 disconnect_and_reconnect(&client_c, cx_c).await;
6003 executor.run_until_parked();
6004 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6005 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6006 assert_eq!(
6007 client_b.summarize_contacts(cx_b).incoming_requests,
6008 &["user_c"]
6009 );
6010 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6011 assert_eq!(
6012 client_c.summarize_contacts(cx_c).outgoing_requests,
6013 &["user_b"]
6014 );
6015
6016 // User B rejects the request from user C.
6017 client_b
6018 .user_store()
6019 .update(cx_b, |store, cx| {
6020 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
6021 })
6022 .await
6023 .unwrap();
6024
6025 executor.run_until_parked();
6026
6027 // User B doesn't see user C as their contact, and the incoming request from them is removed.
6028 let contacts_b = client_b.summarize_contacts(cx_b);
6029 assert_eq!(contacts_b.current, &["user_a"]);
6030 assert!(contacts_b.incoming_requests.is_empty());
6031 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6032 assert_eq!(contacts_b2.current, &["user_a"]);
6033 assert!(contacts_b2.incoming_requests.is_empty());
6034
6035 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
6036 let contacts_c = client_c.summarize_contacts(cx_c);
6037 assert!(contacts_c.current.is_empty());
6038 assert!(contacts_c.outgoing_requests.is_empty());
6039 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
6040 assert!(contacts_c2.current.is_empty());
6041 assert!(contacts_c2.outgoing_requests.is_empty());
6042
6043 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
6044 disconnect_and_reconnect(&client_a, cx_a).await;
6045 disconnect_and_reconnect(&client_b, cx_b).await;
6046 disconnect_and_reconnect(&client_c, cx_c).await;
6047 executor.run_until_parked();
6048 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6049 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6050 assert!(client_b
6051 .summarize_contacts(cx_b)
6052 .incoming_requests
6053 .is_empty());
6054 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6055 assert!(client_c
6056 .summarize_contacts(cx_c)
6057 .outgoing_requests
6058 .is_empty());
6059
6060 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
6061 client.disconnect(&cx.to_async());
6062 client.clear_contacts(cx).await;
6063 client
6064 .authenticate_and_connect(false, &cx.to_async())
6065 .await
6066 .unwrap();
6067 }
6068}
6069
6070// TODO: Re-enable this test once we can replace our swift Livekit SDK with the rust SDK
6071#[cfg(not(target_os = "macos"))]
6072#[gpui::test(iterations = 10)]
6073async fn test_join_call_after_screen_was_shared(
6074 executor: BackgroundExecutor,
6075 cx_a: &mut TestAppContext,
6076 cx_b: &mut TestAppContext,
6077) {
6078 let mut server = TestServer::start(executor.clone()).await;
6079
6080 let client_a = server.create_client(cx_a, "user_a").await;
6081 let client_b = server.create_client(cx_b, "user_b").await;
6082 server
6083 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6084 .await;
6085
6086 let active_call_a = cx_a.read(ActiveCall::global);
6087 let active_call_b = cx_b.read(ActiveCall::global);
6088
6089 // Call users B and C from client A.
6090 active_call_a
6091 .update(cx_a, |call, cx| {
6092 call.invite(client_b.user_id().unwrap(), None, cx)
6093 })
6094 .await
6095 .unwrap();
6096
6097 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
6098 executor.run_until_parked();
6099 assert_eq!(
6100 room_participants(&room_a, cx_a),
6101 RoomParticipants {
6102 remote: Default::default(),
6103 pending: vec!["user_b".to_string()]
6104 }
6105 );
6106
6107 // User B receives the call.
6108
6109 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
6110 let call_b = incoming_call_b.next().await.unwrap().unwrap();
6111 assert_eq!(call_b.calling_user.github_login, "user_a");
6112
6113 // User A shares their screen
6114 let display = gpui::TestScreenCaptureSource::new();
6115 cx_a.set_screen_capture_sources(vec![display]);
6116 active_call_a
6117 .update(cx_a, |call, cx| {
6118 call.room()
6119 .unwrap()
6120 .update(cx, |room, cx| room.share_screen(cx))
6121 })
6122 .await
6123 .unwrap();
6124
6125 client_b.user_store().update(cx_b, |user_store, _| {
6126 user_store.clear_cache();
6127 });
6128
6129 // User B joins the room
6130 active_call_b
6131 .update(cx_b, |call, cx| call.accept_incoming(cx))
6132 .await
6133 .unwrap();
6134
6135 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
6136 assert!(incoming_call_b.next().await.unwrap().is_none());
6137
6138 executor.run_until_parked();
6139 assert_eq!(
6140 room_participants(&room_a, cx_a),
6141 RoomParticipants {
6142 remote: vec!["user_b".to_string()],
6143 pending: vec![],
6144 }
6145 );
6146 assert_eq!(
6147 room_participants(&room_b, cx_b),
6148 RoomParticipants {
6149 remote: vec!["user_a".to_string()],
6150 pending: vec![],
6151 }
6152 );
6153
6154 // Ensure User B sees User A's screenshare.
6155
6156 room_b.read_with(cx_b, |room, _| {
6157 assert_eq!(
6158 room.remote_participants()
6159 .get(&client_a.user_id().unwrap())
6160 .unwrap()
6161 .video_tracks
6162 .len(),
6163 1
6164 );
6165 });
6166}
6167
6168#[gpui::test]
6169async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
6170 let mut server = TestServer::start(cx.executor().clone()).await;
6171 let client_a = server.create_client(cx, "user_a").await;
6172 let (_workspace_a, cx) = client_a.build_test_workspace(cx).await;
6173
6174 cx.simulate_resize(size(px(300.), px(300.)));
6175
6176 cx.simulate_keystrokes("cmd-n cmd-n cmd-n");
6177 cx.update(|cx| cx.refresh());
6178
6179 let tab_bounds = cx.debug_bounds("TAB-2").unwrap();
6180 let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap();
6181
6182 assert!(
6183 tab_bounds.intersects(&new_tab_button_bounds),
6184 "Tab should overlap with the new tab button, if this is failing check if there's been a redesign!"
6185 );
6186
6187 cx.simulate_event(MouseDownEvent {
6188 button: MouseButton::Right,
6189 position: new_tab_button_bounds.center(),
6190 modifiers: Modifiers::default(),
6191 click_count: 1,
6192 first_mouse: false,
6193 });
6194
6195 // regression test that the right click menu for tabs does not open.
6196 assert!(cx.debug_bounds("MENU_ITEM-Close").is_none());
6197
6198 let tab_bounds = cx.debug_bounds("TAB-1").unwrap();
6199 cx.simulate_event(MouseDownEvent {
6200 button: MouseButton::Right,
6201 position: tab_bounds.center(),
6202 modifiers: Modifiers::default(),
6203 click_count: 1,
6204 first_mouse: false,
6205 });
6206 assert!(cx.debug_bounds("MENU_ITEM-Close").is_some());
6207}
6208
6209#[gpui::test]
6210async fn test_pane_split_left(cx: &mut TestAppContext) {
6211 let (_, client) = TestServer::start1(cx).await;
6212 let (workspace, cx) = client.build_test_workspace(cx).await;
6213
6214 cx.simulate_keystrokes("cmd-n");
6215 workspace.update(cx, |workspace, cx| {
6216 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 1);
6217 });
6218 cx.simulate_keystrokes("cmd-k left");
6219 workspace.update(cx, |workspace, cx| {
6220 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
6221 });
6222 cx.simulate_keystrokes("cmd-k");
6223 // sleep for longer than the timeout in keyboard shortcut handling
6224 // to verify that it doesn't fire in this case.
6225 cx.executor().advance_clock(Duration::from_secs(2));
6226 cx.simulate_keystrokes("left");
6227 workspace.update(cx, |workspace, cx| {
6228 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
6229 });
6230}
6231
6232#[gpui::test]
6233async fn test_join_after_restart(cx1: &mut TestAppContext, cx2: &mut TestAppContext) {
6234 let (mut server, client) = TestServer::start1(cx1).await;
6235 let channel1 = server.make_public_channel("channel1", &client, cx1).await;
6236 let channel2 = server.make_public_channel("channel2", &client, cx1).await;
6237
6238 join_channel(channel1, &client, cx1).await.unwrap();
6239 drop(client);
6240
6241 let client2 = server.create_client(cx2, "user_a").await;
6242 join_channel(channel2, &client2, cx2).await.unwrap();
6243}
6244
6245#[gpui::test]
6246async fn test_preview_tabs(cx: &mut TestAppContext) {
6247 let (_server, client) = TestServer::start1(cx).await;
6248 let (workspace, cx) = client.build_test_workspace(cx).await;
6249 let project = workspace.update(cx, |workspace, _| workspace.project().clone());
6250
6251 let worktree_id = project.update(cx, |project, cx| {
6252 project.worktrees(cx).next().unwrap().read(cx).id()
6253 });
6254
6255 let path_1 = ProjectPath {
6256 worktree_id,
6257 path: Path::new("1.txt").into(),
6258 };
6259 let path_2 = ProjectPath {
6260 worktree_id,
6261 path: Path::new("2.js").into(),
6262 };
6263 let path_3 = ProjectPath {
6264 worktree_id,
6265 path: Path::new("3.rs").into(),
6266 };
6267
6268 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6269
6270 let get_path = |pane: &Pane, idx: usize, cx: &AppContext| {
6271 pane.item_for_index(idx).unwrap().project_path(cx).unwrap()
6272 };
6273
6274 // Opening item 3 as a "permanent" tab
6275 workspace
6276 .update(cx, |workspace, cx| {
6277 workspace.open_path(path_3.clone(), None, false, cx)
6278 })
6279 .await
6280 .unwrap();
6281
6282 pane.update(cx, |pane, cx| {
6283 assert_eq!(pane.items_len(), 1);
6284 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6285 assert_eq!(pane.preview_item_id(), None);
6286
6287 assert!(!pane.can_navigate_backward());
6288 assert!(!pane.can_navigate_forward());
6289 });
6290
6291 // Open item 1 as preview
6292 workspace
6293 .update(cx, |workspace, cx| {
6294 workspace.open_path_preview(path_1.clone(), None, true, true, cx)
6295 })
6296 .await
6297 .unwrap();
6298
6299 pane.update(cx, |pane, cx| {
6300 assert_eq!(pane.items_len(), 2);
6301 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6302 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6303 assert_eq!(
6304 pane.preview_item_id(),
6305 Some(pane.items().nth(1).unwrap().item_id())
6306 );
6307
6308 assert!(pane.can_navigate_backward());
6309 assert!(!pane.can_navigate_forward());
6310 });
6311
6312 // Open item 2 as preview
6313 workspace
6314 .update(cx, |workspace, cx| {
6315 workspace.open_path_preview(path_2.clone(), None, true, true, cx)
6316 })
6317 .await
6318 .unwrap();
6319
6320 pane.update(cx, |pane, cx| {
6321 assert_eq!(pane.items_len(), 2);
6322 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6323 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6324 assert_eq!(
6325 pane.preview_item_id(),
6326 Some(pane.items().nth(1).unwrap().item_id())
6327 );
6328
6329 assert!(pane.can_navigate_backward());
6330 assert!(!pane.can_navigate_forward());
6331 });
6332
6333 // Going back should show item 1 as preview
6334 workspace
6335 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6336 .await
6337 .unwrap();
6338
6339 pane.update(cx, |pane, cx| {
6340 assert_eq!(pane.items_len(), 2);
6341 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6342 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6343 assert_eq!(
6344 pane.preview_item_id(),
6345 Some(pane.items().nth(1).unwrap().item_id())
6346 );
6347
6348 assert!(pane.can_navigate_backward());
6349 assert!(pane.can_navigate_forward());
6350 });
6351
6352 // Closing item 1
6353 pane.update(cx, |pane, cx| {
6354 pane.close_item_by_id(
6355 pane.active_item().unwrap().item_id(),
6356 workspace::SaveIntent::Skip,
6357 cx,
6358 )
6359 })
6360 .await
6361 .unwrap();
6362
6363 pane.update(cx, |pane, cx| {
6364 assert_eq!(pane.items_len(), 1);
6365 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6366 assert_eq!(pane.preview_item_id(), None);
6367
6368 assert!(pane.can_navigate_backward());
6369 assert!(!pane.can_navigate_forward());
6370 });
6371
6372 // Going back should show item 1 as preview
6373 workspace
6374 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6375 .await
6376 .unwrap();
6377
6378 pane.update(cx, |pane, cx| {
6379 assert_eq!(pane.items_len(), 2);
6380 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6381 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6382 assert_eq!(
6383 pane.preview_item_id(),
6384 Some(pane.items().nth(1).unwrap().item_id())
6385 );
6386
6387 assert!(pane.can_navigate_backward());
6388 assert!(pane.can_navigate_forward());
6389 });
6390
6391 // Close permanent tab
6392 pane.update(cx, |pane, cx| {
6393 let id = pane.items().next().unwrap().item_id();
6394 pane.close_item_by_id(id, workspace::SaveIntent::Skip, cx)
6395 })
6396 .await
6397 .unwrap();
6398
6399 pane.update(cx, |pane, cx| {
6400 assert_eq!(pane.items_len(), 1);
6401 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6402 assert_eq!(
6403 pane.preview_item_id(),
6404 Some(pane.items().next().unwrap().item_id())
6405 );
6406
6407 assert!(pane.can_navigate_backward());
6408 assert!(pane.can_navigate_forward());
6409 });
6410
6411 // Split pane to the right
6412 pane.update(cx, |pane, cx| {
6413 pane.split(workspace::SplitDirection::Right, cx);
6414 });
6415
6416 let right_pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6417
6418 pane.update(cx, |pane, cx| {
6419 assert_eq!(pane.items_len(), 1);
6420 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6421 assert_eq!(
6422 pane.preview_item_id(),
6423 Some(pane.items().next().unwrap().item_id())
6424 );
6425
6426 assert!(pane.can_navigate_backward());
6427 assert!(pane.can_navigate_forward());
6428 });
6429
6430 right_pane.update(cx, |pane, cx| {
6431 assert_eq!(pane.items_len(), 1);
6432 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6433 assert_eq!(pane.preview_item_id(), None);
6434
6435 assert!(!pane.can_navigate_backward());
6436 assert!(!pane.can_navigate_forward());
6437 });
6438
6439 // Open item 2 as preview in right pane
6440 workspace
6441 .update(cx, |workspace, cx| {
6442 workspace.open_path_preview(path_2.clone(), None, true, true, cx)
6443 })
6444 .await
6445 .unwrap();
6446
6447 pane.update(cx, |pane, cx| {
6448 assert_eq!(pane.items_len(), 1);
6449 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6450 assert_eq!(
6451 pane.preview_item_id(),
6452 Some(pane.items().next().unwrap().item_id())
6453 );
6454
6455 assert!(pane.can_navigate_backward());
6456 assert!(pane.can_navigate_forward());
6457 });
6458
6459 right_pane.update(cx, |pane, cx| {
6460 assert_eq!(pane.items_len(), 2);
6461 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6462 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6463 assert_eq!(
6464 pane.preview_item_id(),
6465 Some(pane.items().nth(1).unwrap().item_id())
6466 );
6467
6468 assert!(pane.can_navigate_backward());
6469 assert!(!pane.can_navigate_forward());
6470 });
6471
6472 // Focus left pane
6473 workspace.update(cx, |workspace, cx| {
6474 workspace.activate_pane_in_direction(workspace::SplitDirection::Left, cx)
6475 });
6476
6477 // Open item 2 as preview in left pane
6478 workspace
6479 .update(cx, |workspace, cx| {
6480 workspace.open_path_preview(path_2.clone(), None, true, true, cx)
6481 })
6482 .await
6483 .unwrap();
6484
6485 pane.update(cx, |pane, cx| {
6486 assert_eq!(pane.items_len(), 1);
6487 assert_eq!(get_path(pane, 0, cx), path_2.clone());
6488 assert_eq!(
6489 pane.preview_item_id(),
6490 Some(pane.items().next().unwrap().item_id())
6491 );
6492
6493 assert!(pane.can_navigate_backward());
6494 assert!(!pane.can_navigate_forward());
6495 });
6496
6497 right_pane.update(cx, |pane, cx| {
6498 assert_eq!(pane.items_len(), 2);
6499 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6500 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6501 assert_eq!(
6502 pane.preview_item_id(),
6503 Some(pane.items().nth(1).unwrap().item_id())
6504 );
6505
6506 assert!(pane.can_navigate_backward());
6507 assert!(!pane.can_navigate_forward());
6508 });
6509}
6510
6511#[gpui::test(iterations = 10)]
6512async fn test_context_collaboration_with_reconnect(
6513 executor: BackgroundExecutor,
6514 cx_a: &mut TestAppContext,
6515 cx_b: &mut TestAppContext,
6516) {
6517 let mut server = TestServer::start(executor.clone()).await;
6518 let client_a = server.create_client(cx_a, "user_a").await;
6519 let client_b = server.create_client(cx_b, "user_b").await;
6520 server
6521 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6522 .await;
6523 let active_call_a = cx_a.read(ActiveCall::global);
6524
6525 client_a.fs().insert_tree("/a", Default::default()).await;
6526 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
6527 let project_id = active_call_a
6528 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6529 .await
6530 .unwrap();
6531 let project_b = client_b.join_remote_project(project_id, cx_b).await;
6532
6533 // Client A sees that a guest has joined.
6534 executor.run_until_parked();
6535
6536 project_a.read_with(cx_a, |project, _| {
6537 assert_eq!(project.collaborators().len(), 1);
6538 });
6539 project_b.read_with(cx_b, |project, _| {
6540 assert_eq!(project.collaborators().len(), 1);
6541 });
6542
6543 cx_a.update(context_server::init);
6544 cx_b.update(context_server::init);
6545 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
6546 let context_store_a = cx_a
6547 .update(|cx| {
6548 ContextStore::new(
6549 project_a.clone(),
6550 prompt_builder.clone(),
6551 Arc::new(SlashCommandWorkingSet::default()),
6552 Arc::new(ToolWorkingSet::default()),
6553 cx,
6554 )
6555 })
6556 .await
6557 .unwrap();
6558 let context_store_b = cx_b
6559 .update(|cx| {
6560 ContextStore::new(
6561 project_b.clone(),
6562 prompt_builder.clone(),
6563 Arc::new(SlashCommandWorkingSet::default()),
6564 Arc::new(ToolWorkingSet::default()),
6565 cx,
6566 )
6567 })
6568 .await
6569 .unwrap();
6570
6571 // Client A creates a new chats.
6572 let context_a = context_store_a.update(cx_a, |store, cx| store.create(cx));
6573 executor.run_until_parked();
6574
6575 // Client B retrieves host's contexts and joins one.
6576 let context_b = context_store_b
6577 .update(cx_b, |store, cx| {
6578 let host_contexts = store.host_contexts().to_vec();
6579 assert_eq!(host_contexts.len(), 1);
6580 store.open_remote_context(host_contexts[0].id.clone(), cx)
6581 })
6582 .await
6583 .unwrap();
6584
6585 // Host and guest make changes
6586 context_a.update(cx_a, |context, cx| {
6587 context.buffer().update(cx, |buffer, cx| {
6588 buffer.edit([(0..0, "Host change\n")], None, cx)
6589 })
6590 });
6591 context_b.update(cx_b, |context, cx| {
6592 context.buffer().update(cx, |buffer, cx| {
6593 buffer.edit([(0..0, "Guest change\n")], None, cx)
6594 })
6595 });
6596 executor.run_until_parked();
6597 assert_eq!(
6598 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6599 "Guest change\nHost change\n"
6600 );
6601 assert_eq!(
6602 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6603 "Guest change\nHost change\n"
6604 );
6605
6606 // Disconnect client A and make some changes while disconnected.
6607 server.disconnect_client(client_a.peer_id().unwrap());
6608 server.forbid_connections();
6609 context_a.update(cx_a, |context, cx| {
6610 context.buffer().update(cx, |buffer, cx| {
6611 buffer.edit([(0..0, "Host offline change\n")], None, cx)
6612 })
6613 });
6614 context_b.update(cx_b, |context, cx| {
6615 context.buffer().update(cx, |buffer, cx| {
6616 buffer.edit([(0..0, "Guest offline change\n")], None, cx)
6617 })
6618 });
6619 executor.run_until_parked();
6620 assert_eq!(
6621 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6622 "Host offline change\nGuest change\nHost change\n"
6623 );
6624 assert_eq!(
6625 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6626 "Guest offline change\nGuest change\nHost change\n"
6627 );
6628
6629 // Allow client A to reconnect and verify that contexts converge.
6630 server.allow_connections();
6631 executor.advance_clock(RECEIVE_TIMEOUT);
6632 assert_eq!(
6633 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6634 "Guest offline change\nHost offline change\nGuest change\nHost change\n"
6635 );
6636 assert_eq!(
6637 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6638 "Guest offline change\nHost offline change\nGuest change\nHost change\n"
6639 );
6640
6641 // Client A disconnects without being able to reconnect. Context B becomes readonly.
6642 server.forbid_connections();
6643 server.disconnect_client(client_a.peer_id().unwrap());
6644 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
6645 context_b.read_with(cx_b, |context, cx| {
6646 assert!(context.buffer().read(cx).read_only());
6647 });
6648}
6649
6650#[gpui::test]
6651async fn test_remote_git_branches(
6652 executor: BackgroundExecutor,
6653 cx_a: &mut TestAppContext,
6654 cx_b: &mut TestAppContext,
6655) {
6656 let mut server = TestServer::start(executor.clone()).await;
6657 let client_a = server.create_client(cx_a, "user_a").await;
6658 let client_b = server.create_client(cx_b, "user_b").await;
6659 server
6660 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6661 .await;
6662 let active_call_a = cx_a.read(ActiveCall::global);
6663
6664 client_a
6665 .fs()
6666 .insert_tree("/project", serde_json::json!({ ".git":{} }))
6667 .await;
6668 let branches = ["main", "dev", "feature-1"];
6669 client_a
6670 .fs()
6671 .insert_branches(Path::new("/project/.git"), &branches);
6672
6673 let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await;
6674 let project_id = active_call_a
6675 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6676 .await
6677 .unwrap();
6678 let project_b = client_b.join_remote_project(project_id, cx_b).await;
6679
6680 let root_path = ProjectPath::root_path(worktree_id);
6681 // Client A sees that a guest has joined.
6682 executor.run_until_parked();
6683
6684 let branches_b = cx_b
6685 .update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
6686 .await
6687 .unwrap();
6688
6689 let new_branch = branches[2];
6690
6691 let branches_b = branches_b
6692 .into_iter()
6693 .map(|branch| branch.name)
6694 .collect::<Vec<_>>();
6695
6696 assert_eq!(&branches_b, &branches);
6697
6698 cx_b.update(|cx| {
6699 project_b.update(cx, |project, cx| {
6700 project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
6701 })
6702 })
6703 .await
6704 .unwrap();
6705
6706 executor.run_until_parked();
6707
6708 let host_branch = cx_a.update(|cx| {
6709 project_a.update(cx, |project, cx| {
6710 project.worktree_store().update(cx, |worktree_store, cx| {
6711 worktree_store
6712 .current_branch(root_path.clone(), cx)
6713 .unwrap()
6714 })
6715 })
6716 });
6717
6718 assert_eq!(host_branch.as_ref(), branches[2]);
6719
6720 // Also try creating a new branch
6721 cx_b.update(|cx| {
6722 project_b.update(cx, |project, cx| {
6723 project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
6724 })
6725 })
6726 .await
6727 .unwrap();
6728
6729 executor.run_until_parked();
6730
6731 let host_branch = cx_a.update(|cx| {
6732 project_a.update(cx, |project, cx| {
6733 project.worktree_store().update(cx, |worktree_store, cx| {
6734 worktree_store.current_branch(root_path, cx).unwrap()
6735 })
6736 })
6737 });
6738
6739 assert_eq!(host_branch.as_ref(), "totally-new-branch");
6740}