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