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