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