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