1use crate::{
2 rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
3 tests::{
4 channel_id, following_tests::join_channel, room_participants, rust_lang, RoomParticipants,
5 TestClient, TestServer,
6 },
7};
8use anyhow::{anyhow, Result};
9use assistant::{ContextStore, PromptBuilder, SlashCommandWorkingSet, ToolWorkingSet};
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, TestScreenCaptureSource, UpdateGlobal,
19};
20use language::{
21 language_settings::{
22 AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
23 },
24 tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticEntry, FakeLspAdapter,
25 Language, LanguageConfig, LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
26};
27use lsp::LanguageServerId;
28use parking_lot::Mutex;
29use project::lsp_store::FormatTarget;
30use project::{
31 lsp_store::FormatTrigger, search::SearchQuery, search::SearchResult, DiagnosticSummary,
32 HoverBlockKind, Project, ProjectPath,
33};
34use rand::prelude::*;
35use serde_json::json;
36use settings::SettingsStore;
37use std::{
38 cell::{Cell, RefCell},
39 env, future, mem,
40 path::{Path, PathBuf},
41 rc::Rc,
42 sync::{
43 atomic::{AtomicBool, Ordering::SeqCst},
44 Arc,
45 },
46 time::Duration,
47};
48use unindent::Unindent as _;
49use workspace::Pane;
50
51#[ctor::ctor]
52fn init_logger() {
53 if std::env::var("RUST_LOG").is_ok() {
54 env_logger::init();
55 }
56}
57
58#[gpui::test(iterations = 10)]
59async fn test_basic_calls(
60 executor: BackgroundExecutor,
61 cx_a: &mut TestAppContext,
62 cx_b: &mut TestAppContext,
63 cx_b2: &mut TestAppContext,
64 cx_c: &mut TestAppContext,
65) {
66 let mut server = TestServer::start(executor.clone()).await;
67
68 let client_a = server.create_client(cx_a, "user_a").await;
69 let client_b = server.create_client(cx_b, "user_b").await;
70 let client_c = server.create_client(cx_c, "user_c").await;
71 server
72 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
73 .await;
74
75 let active_call_a = cx_a.read(ActiveCall::global);
76 let active_call_b = cx_b.read(ActiveCall::global);
77 let active_call_c = cx_c.read(ActiveCall::global);
78
79 // Call user B from client A.
80 active_call_a
81 .update(cx_a, |call, cx| {
82 call.invite(client_b.user_id().unwrap(), None, cx)
83 })
84 .await
85 .unwrap();
86 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
87 executor.run_until_parked();
88 assert_eq!(
89 room_participants(&room_a, cx_a),
90 RoomParticipants {
91 remote: Default::default(),
92 pending: vec!["user_b".to_string()]
93 }
94 );
95
96 // User B receives the call.
97
98 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
99 let call_b = incoming_call_b.next().await.unwrap().unwrap();
100 assert_eq!(call_b.calling_user.github_login, "user_a");
101
102 // User B connects via another client and also receives a ring on the newly-connected client.
103 let _client_b2 = server.create_client(cx_b2, "user_b").await;
104 let active_call_b2 = cx_b2.read(ActiveCall::global);
105
106 let mut incoming_call_b2 = active_call_b2.read_with(cx_b2, |call, _| call.incoming());
107 executor.run_until_parked();
108 let call_b2 = incoming_call_b2.next().await.unwrap().unwrap();
109 assert_eq!(call_b2.calling_user.github_login, "user_a");
110
111 // User B joins the room using the first client.
112 active_call_b
113 .update(cx_b, |call, cx| call.accept_incoming(cx))
114 .await
115 .unwrap();
116
117 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
118 assert!(incoming_call_b.next().await.unwrap().is_none());
119
120 executor.run_until_parked();
121 assert_eq!(
122 room_participants(&room_a, cx_a),
123 RoomParticipants {
124 remote: vec!["user_b".to_string()],
125 pending: Default::default()
126 }
127 );
128 assert_eq!(
129 room_participants(&room_b, cx_b),
130 RoomParticipants {
131 remote: vec!["user_a".to_string()],
132 pending: Default::default()
133 }
134 );
135
136 // Call user C from client B.
137
138 let mut incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
139 active_call_b
140 .update(cx_b, |call, cx| {
141 call.invite(client_c.user_id().unwrap(), None, cx)
142 })
143 .await
144 .unwrap();
145
146 executor.run_until_parked();
147 assert_eq!(
148 room_participants(&room_a, cx_a),
149 RoomParticipants {
150 remote: vec!["user_b".to_string()],
151 pending: vec!["user_c".to_string()]
152 }
153 );
154 assert_eq!(
155 room_participants(&room_b, cx_b),
156 RoomParticipants {
157 remote: vec!["user_a".to_string()],
158 pending: vec!["user_c".to_string()]
159 }
160 );
161
162 // User C receives the call, but declines it.
163 let call_c = incoming_call_c.next().await.unwrap().unwrap();
164 assert_eq!(call_c.calling_user.github_login, "user_b");
165 active_call_c.update(cx_c, |call, cx| call.decline_incoming(cx).unwrap());
166 assert!(incoming_call_c.next().await.unwrap().is_none());
167
168 executor.run_until_parked();
169 assert_eq!(
170 room_participants(&room_a, cx_a),
171 RoomParticipants {
172 remote: vec!["user_b".to_string()],
173 pending: Default::default()
174 }
175 );
176 assert_eq!(
177 room_participants(&room_b, cx_b),
178 RoomParticipants {
179 remote: vec!["user_a".to_string()],
180 pending: Default::default()
181 }
182 );
183
184 // Call user C again from user A.
185 active_call_a
186 .update(cx_a, |call, cx| {
187 call.invite(client_c.user_id().unwrap(), None, cx)
188 })
189 .await
190 .unwrap();
191
192 executor.run_until_parked();
193 assert_eq!(
194 room_participants(&room_a, cx_a),
195 RoomParticipants {
196 remote: vec!["user_b".to_string()],
197 pending: vec!["user_c".to_string()]
198 }
199 );
200 assert_eq!(
201 room_participants(&room_b, cx_b),
202 RoomParticipants {
203 remote: vec!["user_a".to_string()],
204 pending: vec!["user_c".to_string()]
205 }
206 );
207
208 // User C accepts the call.
209 let call_c = incoming_call_c.next().await.unwrap().unwrap();
210 assert_eq!(call_c.calling_user.github_login, "user_a");
211 active_call_c
212 .update(cx_c, |call, cx| call.accept_incoming(cx))
213 .await
214 .unwrap();
215 assert!(incoming_call_c.next().await.unwrap().is_none());
216
217 let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
218
219 executor.run_until_parked();
220 assert_eq!(
221 room_participants(&room_a, cx_a),
222 RoomParticipants {
223 remote: vec!["user_b".to_string(), "user_c".to_string()],
224 pending: Default::default()
225 }
226 );
227 assert_eq!(
228 room_participants(&room_b, cx_b),
229 RoomParticipants {
230 remote: vec!["user_a".to_string(), "user_c".to_string()],
231 pending: Default::default()
232 }
233 );
234 assert_eq!(
235 room_participants(&room_c, cx_c),
236 RoomParticipants {
237 remote: vec!["user_a".to_string(), "user_b".to_string()],
238 pending: Default::default()
239 }
240 );
241
242 // User A shares their screen
243 let display = TestScreenCaptureSource::new();
244 let events_b = active_call_events(cx_b);
245 let events_c = active_call_events(cx_c);
246 cx_a.set_screen_capture_sources(vec![display]);
247 active_call_a
248 .update(cx_a, |call, cx| {
249 call.room()
250 .unwrap()
251 .update(cx, |room, cx| room.share_screen(cx))
252 })
253 .await
254 .unwrap();
255
256 executor.run_until_parked();
257
258 // User B observes the remote screen sharing track.
259 assert_eq!(events_b.borrow().len(), 1);
260 let event_b = events_b.borrow().first().unwrap().clone();
261 if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b {
262 assert_eq!(participant_id, client_a.peer_id().unwrap());
263
264 room_b.read_with(cx_b, |room, _| {
265 assert_eq!(
266 room.remote_participants()[&client_a.user_id().unwrap()]
267 .video_tracks
268 .len(),
269 1
270 );
271 });
272 } else {
273 panic!("unexpected event")
274 }
275
276 // User C observes the remote screen sharing track.
277 assert_eq!(events_c.borrow().len(), 1);
278 let event_c = events_c.borrow().first().unwrap().clone();
279 if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c {
280 assert_eq!(participant_id, client_a.peer_id().unwrap());
281
282 room_c.read_with(cx_c, |room, _| {
283 assert_eq!(
284 room.remote_participants()[&client_a.user_id().unwrap()]
285 .video_tracks
286 .len(),
287 1
288 );
289 });
290 } else {
291 panic!("unexpected event")
292 }
293
294 // User A leaves the room.
295 active_call_a
296 .update(cx_a, |call, cx| {
297 let hang_up = call.hang_up(cx);
298 assert!(call.room().is_none());
299 hang_up
300 })
301 .await
302 .unwrap();
303 executor.run_until_parked();
304 assert_eq!(
305 room_participants(&room_a, cx_a),
306 RoomParticipants {
307 remote: Default::default(),
308 pending: Default::default()
309 }
310 );
311 assert_eq!(
312 room_participants(&room_b, cx_b),
313 RoomParticipants {
314 remote: vec!["user_c".to_string()],
315 pending: Default::default()
316 }
317 );
318 assert_eq!(
319 room_participants(&room_c, cx_c),
320 RoomParticipants {
321 remote: vec!["user_b".to_string()],
322 pending: Default::default()
323 }
324 );
325
326 // User B gets disconnected from the LiveKit server, which causes them
327 // to automatically leave the room. User C leaves the room as well because
328 // nobody else is in there.
329 server
330 .test_live_kit_server
331 .disconnect_client(client_b.user_id().unwrap().to_string())
332 .await;
333 executor.run_until_parked();
334
335 active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none()));
336
337 active_call_c.read_with(cx_c, |call, _| assert!(call.room().is_none()));
338 assert_eq!(
339 room_participants(&room_a, cx_a),
340 RoomParticipants {
341 remote: Default::default(),
342 pending: Default::default()
343 }
344 );
345 assert_eq!(
346 room_participants(&room_b, cx_b),
347 RoomParticipants {
348 remote: Default::default(),
349 pending: Default::default()
350 }
351 );
352 assert_eq!(
353 room_participants(&room_c, cx_c),
354 RoomParticipants {
355 remote: Default::default(),
356 pending: Default::default()
357 }
358 );
359}
360
361#[gpui::test(iterations = 10)]
362async fn test_calling_multiple_users_simultaneously(
363 executor: BackgroundExecutor,
364 cx_a: &mut TestAppContext,
365 cx_b: &mut TestAppContext,
366 cx_c: &mut TestAppContext,
367 cx_d: &mut TestAppContext,
368) {
369 let mut server = TestServer::start(executor.clone()).await;
370
371 let client_a = server.create_client(cx_a, "user_a").await;
372 let client_b = server.create_client(cx_b, "user_b").await;
373 let client_c = server.create_client(cx_c, "user_c").await;
374 let client_d = server.create_client(cx_d, "user_d").await;
375 server
376 .make_contacts(&mut [
377 (&client_a, cx_a),
378 (&client_b, cx_b),
379 (&client_c, cx_c),
380 (&client_d, cx_d),
381 ])
382 .await;
383
384 let active_call_a = cx_a.read(ActiveCall::global);
385 let active_call_b = cx_b.read(ActiveCall::global);
386 let active_call_c = cx_c.read(ActiveCall::global);
387 let active_call_d = cx_d.read(ActiveCall::global);
388
389 // Simultaneously call user B and user C from client A.
390 let b_invite = active_call_a.update(cx_a, |call, cx| {
391 call.invite(client_b.user_id().unwrap(), None, cx)
392 });
393 let c_invite = active_call_a.update(cx_a, |call, cx| {
394 call.invite(client_c.user_id().unwrap(), None, cx)
395 });
396 b_invite.await.unwrap();
397 c_invite.await.unwrap();
398
399 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
400 executor.run_until_parked();
401 assert_eq!(
402 room_participants(&room_a, cx_a),
403 RoomParticipants {
404 remote: Default::default(),
405 pending: vec!["user_b".to_string(), "user_c".to_string()]
406 }
407 );
408
409 // Call client D from client A.
410 active_call_a
411 .update(cx_a, |call, cx| {
412 call.invite(client_d.user_id().unwrap(), None, cx)
413 })
414 .await
415 .unwrap();
416 executor.run_until_parked();
417 assert_eq!(
418 room_participants(&room_a, cx_a),
419 RoomParticipants {
420 remote: Default::default(),
421 pending: vec![
422 "user_b".to_string(),
423 "user_c".to_string(),
424 "user_d".to_string()
425 ]
426 }
427 );
428
429 // Accept the call on all clients simultaneously.
430 let accept_b = active_call_b.update(cx_b, |call, cx| call.accept_incoming(cx));
431 let accept_c = active_call_c.update(cx_c, |call, cx| call.accept_incoming(cx));
432 let accept_d = active_call_d.update(cx_d, |call, cx| call.accept_incoming(cx));
433 accept_b.await.unwrap();
434 accept_c.await.unwrap();
435 accept_d.await.unwrap();
436
437 executor.run_until_parked();
438
439 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
440
441 let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
442
443 let room_d = active_call_d.read_with(cx_d, |call, _| call.room().unwrap().clone());
444 assert_eq!(
445 room_participants(&room_a, cx_a),
446 RoomParticipants {
447 remote: vec![
448 "user_b".to_string(),
449 "user_c".to_string(),
450 "user_d".to_string(),
451 ],
452 pending: Default::default()
453 }
454 );
455 assert_eq!(
456 room_participants(&room_b, cx_b),
457 RoomParticipants {
458 remote: vec![
459 "user_a".to_string(),
460 "user_c".to_string(),
461 "user_d".to_string(),
462 ],
463 pending: Default::default()
464 }
465 );
466 assert_eq!(
467 room_participants(&room_c, cx_c),
468 RoomParticipants {
469 remote: vec![
470 "user_a".to_string(),
471 "user_b".to_string(),
472 "user_d".to_string(),
473 ],
474 pending: Default::default()
475 }
476 );
477 assert_eq!(
478 room_participants(&room_d, cx_d),
479 RoomParticipants {
480 remote: vec![
481 "user_a".to_string(),
482 "user_b".to_string(),
483 "user_c".to_string(),
484 ],
485 pending: Default::default()
486 }
487 );
488}
489
490#[gpui::test(iterations = 10)]
491async fn test_joining_channels_and_calling_multiple_users_simultaneously(
492 executor: BackgroundExecutor,
493 cx_a: &mut TestAppContext,
494 cx_b: &mut TestAppContext,
495 cx_c: &mut TestAppContext,
496) {
497 let mut server = TestServer::start(executor.clone()).await;
498
499 let client_a = server.create_client(cx_a, "user_a").await;
500 let client_b = server.create_client(cx_b, "user_b").await;
501 let client_c = server.create_client(cx_c, "user_c").await;
502 server
503 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
504 .await;
505
506 let channel_1 = server
507 .make_channel(
508 "channel1",
509 None,
510 (&client_a, cx_a),
511 &mut [(&client_b, cx_b), (&client_c, cx_c)],
512 )
513 .await;
514
515 let channel_2 = server
516 .make_channel(
517 "channel2",
518 None,
519 (&client_a, cx_a),
520 &mut [(&client_b, cx_b), (&client_c, cx_c)],
521 )
522 .await;
523
524 let active_call_a = cx_a.read(ActiveCall::global);
525
526 // Simultaneously join channel 1 and then channel 2
527 active_call_a
528 .update(cx_a, |call, cx| call.join_channel(channel_1, cx))
529 .detach();
530 let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx));
531
532 join_channel_2.await.unwrap();
533
534 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
535 executor.run_until_parked();
536
537 assert_eq!(channel_id(&room_a, cx_a), Some(channel_2));
538
539 // Leave the room
540 active_call_a
541 .update(cx_a, |call, cx| call.hang_up(cx))
542 .await
543 .unwrap();
544
545 // Initiating invites and then joining a channel should fail gracefully
546 let b_invite = active_call_a.update(cx_a, |call, cx| {
547 call.invite(client_b.user_id().unwrap(), None, cx)
548 });
549 let c_invite = active_call_a.update(cx_a, |call, cx| {
550 call.invite(client_c.user_id().unwrap(), None, cx)
551 });
552
553 let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
554
555 b_invite.await.unwrap();
556 c_invite.await.unwrap();
557 join_channel.await.unwrap();
558
559 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
560 executor.run_until_parked();
561
562 assert_eq!(
563 room_participants(&room_a, cx_a),
564 RoomParticipants {
565 remote: Default::default(),
566 pending: vec!["user_b".to_string(), "user_c".to_string()]
567 }
568 );
569
570 assert_eq!(channel_id(&room_a, cx_a), None);
571
572 // Leave the room
573 active_call_a
574 .update(cx_a, |call, cx| call.hang_up(cx))
575 .await
576 .unwrap();
577
578 // Simultaneously join channel 1 and call user B and user C from client A.
579 let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
580
581 let b_invite = active_call_a.update(cx_a, |call, cx| {
582 call.invite(client_b.user_id().unwrap(), None, cx)
583 });
584 let c_invite = active_call_a.update(cx_a, |call, cx| {
585 call.invite(client_c.user_id().unwrap(), None, cx)
586 });
587
588 join_channel.await.unwrap();
589 b_invite.await.unwrap();
590 c_invite.await.unwrap();
591
592 active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
593 executor.run_until_parked();
594}
595
596#[gpui::test(iterations = 10)]
597async fn test_room_uniqueness(
598 executor: BackgroundExecutor,
599 cx_a: &mut TestAppContext,
600 cx_a2: &mut TestAppContext,
601 cx_b: &mut TestAppContext,
602 cx_b2: &mut TestAppContext,
603 cx_c: &mut TestAppContext,
604) {
605 let mut server = TestServer::start(executor.clone()).await;
606 let client_a = server.create_client(cx_a, "user_a").await;
607 let _client_a2 = server.create_client(cx_a2, "user_a").await;
608 let client_b = server.create_client(cx_b, "user_b").await;
609 let _client_b2 = server.create_client(cx_b2, "user_b").await;
610 let client_c = server.create_client(cx_c, "user_c").await;
611 server
612 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
613 .await;
614
615 let active_call_a = cx_a.read(ActiveCall::global);
616 let active_call_a2 = cx_a2.read(ActiveCall::global);
617 let active_call_b = cx_b.read(ActiveCall::global);
618 let active_call_b2 = cx_b2.read(ActiveCall::global);
619 let active_call_c = cx_c.read(ActiveCall::global);
620
621 // Call user B from client A.
622 active_call_a
623 .update(cx_a, |call, cx| {
624 call.invite(client_b.user_id().unwrap(), None, cx)
625 })
626 .await
627 .unwrap();
628
629 // Ensure a new room can't be created given user A just created one.
630 active_call_a2
631 .update(cx_a2, |call, cx| {
632 call.invite(client_c.user_id().unwrap(), None, cx)
633 })
634 .await
635 .unwrap_err();
636
637 active_call_a2.read_with(cx_a2, |call, _| assert!(call.room().is_none()));
638
639 // User B receives the call from user A.
640
641 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
642 let call_b1 = incoming_call_b.next().await.unwrap().unwrap();
643 assert_eq!(call_b1.calling_user.github_login, "user_a");
644
645 // Ensure calling users A and B from client C fails.
646 active_call_c
647 .update(cx_c, |call, cx| {
648 call.invite(client_a.user_id().unwrap(), None, cx)
649 })
650 .await
651 .unwrap_err();
652 active_call_c
653 .update(cx_c, |call, cx| {
654 call.invite(client_b.user_id().unwrap(), None, cx)
655 })
656 .await
657 .unwrap_err();
658
659 // Ensure User B can't create a room while they still have an incoming call.
660 active_call_b2
661 .update(cx_b2, |call, cx| {
662 call.invite(client_c.user_id().unwrap(), None, cx)
663 })
664 .await
665 .unwrap_err();
666
667 active_call_b2.read_with(cx_b2, |call, _| assert!(call.room().is_none()));
668
669 // User B joins the room and calling them after they've joined still fails.
670 active_call_b
671 .update(cx_b, |call, cx| call.accept_incoming(cx))
672 .await
673 .unwrap();
674 active_call_c
675 .update(cx_c, |call, cx| {
676 call.invite(client_b.user_id().unwrap(), None, cx)
677 })
678 .await
679 .unwrap_err();
680
681 // Ensure User B can't create a room while they belong to another room.
682 active_call_b2
683 .update(cx_b2, |call, cx| {
684 call.invite(client_c.user_id().unwrap(), None, cx)
685 })
686 .await
687 .unwrap_err();
688
689 active_call_b2.read_with(cx_b2, |call, _| assert!(call.room().is_none()));
690
691 // Client C can successfully call client B after client B leaves the room.
692 active_call_b
693 .update(cx_b, |call, cx| call.hang_up(cx))
694 .await
695 .unwrap();
696 executor.run_until_parked();
697 active_call_c
698 .update(cx_c, |call, cx| {
699 call.invite(client_b.user_id().unwrap(), None, cx)
700 })
701 .await
702 .unwrap();
703 executor.run_until_parked();
704 let call_b2 = incoming_call_b.next().await.unwrap().unwrap();
705 assert_eq!(call_b2.calling_user.github_login, "user_c");
706}
707
708#[gpui::test(iterations = 10)]
709async fn test_client_disconnecting_from_room(
710 executor: BackgroundExecutor,
711 cx_a: &mut TestAppContext,
712 cx_b: &mut TestAppContext,
713) {
714 let mut server = TestServer::start(executor.clone()).await;
715 let client_a = server.create_client(cx_a, "user_a").await;
716 let client_b = server.create_client(cx_b, "user_b").await;
717 server
718 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
719 .await;
720
721 let active_call_a = cx_a.read(ActiveCall::global);
722 let active_call_b = cx_b.read(ActiveCall::global);
723
724 // Call user B from client A.
725 active_call_a
726 .update(cx_a, |call, cx| {
727 call.invite(client_b.user_id().unwrap(), None, cx)
728 })
729 .await
730 .unwrap();
731
732 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
733
734 // User B receives the call and joins the room.
735
736 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
737 incoming_call_b.next().await.unwrap().unwrap();
738 active_call_b
739 .update(cx_b, |call, cx| call.accept_incoming(cx))
740 .await
741 .unwrap();
742
743 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
744 executor.run_until_parked();
745 assert_eq!(
746 room_participants(&room_a, cx_a),
747 RoomParticipants {
748 remote: vec!["user_b".to_string()],
749 pending: Default::default()
750 }
751 );
752 assert_eq!(
753 room_participants(&room_b, cx_b),
754 RoomParticipants {
755 remote: vec!["user_a".to_string()],
756 pending: Default::default()
757 }
758 );
759
760 // User A automatically reconnects to the room upon disconnection.
761 server.disconnect_client(client_a.peer_id().unwrap());
762 executor.advance_clock(RECEIVE_TIMEOUT);
763 executor.run_until_parked();
764 assert_eq!(
765 room_participants(&room_a, cx_a),
766 RoomParticipants {
767 remote: vec!["user_b".to_string()],
768 pending: Default::default()
769 }
770 );
771 assert_eq!(
772 room_participants(&room_b, cx_b),
773 RoomParticipants {
774 remote: vec!["user_a".to_string()],
775 pending: Default::default()
776 }
777 );
778
779 // When user A disconnects, both client A and B clear their room on the active call.
780 server.forbid_connections();
781 server.disconnect_client(client_a.peer_id().unwrap());
782 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
783
784 active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none()));
785
786 active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none()));
787 assert_eq!(
788 room_participants(&room_a, cx_a),
789 RoomParticipants {
790 remote: Default::default(),
791 pending: Default::default()
792 }
793 );
794 assert_eq!(
795 room_participants(&room_b, cx_b),
796 RoomParticipants {
797 remote: Default::default(),
798 pending: Default::default()
799 }
800 );
801
802 // Allow user A to reconnect to the server.
803 server.allow_connections();
804 executor.advance_clock(RECEIVE_TIMEOUT);
805
806 // Call user B again from client A.
807 active_call_a
808 .update(cx_a, |call, cx| {
809 call.invite(client_b.user_id().unwrap(), None, cx)
810 })
811 .await
812 .unwrap();
813
814 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
815
816 // User B receives the call and joins the room.
817
818 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
819 incoming_call_b.next().await.unwrap().unwrap();
820 active_call_b
821 .update(cx_b, |call, cx| call.accept_incoming(cx))
822 .await
823 .unwrap();
824
825 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
826 executor.run_until_parked();
827 assert_eq!(
828 room_participants(&room_a, cx_a),
829 RoomParticipants {
830 remote: vec!["user_b".to_string()],
831 pending: Default::default()
832 }
833 );
834 assert_eq!(
835 room_participants(&room_b, cx_b),
836 RoomParticipants {
837 remote: vec!["user_a".to_string()],
838 pending: Default::default()
839 }
840 );
841
842 // User B gets disconnected from the LiveKit server, which causes it
843 // to automatically leave the room.
844 server
845 .test_live_kit_server
846 .disconnect_client(client_b.user_id().unwrap().to_string())
847 .await;
848 executor.run_until_parked();
849 active_call_a.update(cx_a, |call, _| assert!(call.room().is_none()));
850 active_call_b.update(cx_b, |call, _| assert!(call.room().is_none()));
851 assert_eq!(
852 room_participants(&room_a, cx_a),
853 RoomParticipants {
854 remote: Default::default(),
855 pending: Default::default()
856 }
857 );
858 assert_eq!(
859 room_participants(&room_b, cx_b),
860 RoomParticipants {
861 remote: Default::default(),
862 pending: Default::default()
863 }
864 );
865}
866
867#[gpui::test(iterations = 10)]
868async fn test_server_restarts(
869 executor: BackgroundExecutor,
870 cx_a: &mut TestAppContext,
871 cx_b: &mut TestAppContext,
872 cx_c: &mut TestAppContext,
873 cx_d: &mut TestAppContext,
874) {
875 let mut server = TestServer::start(executor.clone()).await;
876 let client_a = server.create_client(cx_a, "user_a").await;
877 client_a
878 .fs()
879 .insert_tree("/a", json!({ "a.txt": "a-contents" }))
880 .await;
881
882 // Invite client B to collaborate on a project
883 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
884
885 let client_b = server.create_client(cx_b, "user_b").await;
886 let client_c = server.create_client(cx_c, "user_c").await;
887 let client_d = server.create_client(cx_d, "user_d").await;
888 server
889 .make_contacts(&mut [
890 (&client_a, cx_a),
891 (&client_b, cx_b),
892 (&client_c, cx_c),
893 (&client_d, cx_d),
894 ])
895 .await;
896
897 let active_call_a = cx_a.read(ActiveCall::global);
898 let active_call_b = cx_b.read(ActiveCall::global);
899 let active_call_c = cx_c.read(ActiveCall::global);
900 let active_call_d = cx_d.read(ActiveCall::global);
901
902 // User A calls users B, C, and D.
903 active_call_a
904 .update(cx_a, |call, cx| {
905 call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
906 })
907 .await
908 .unwrap();
909 active_call_a
910 .update(cx_a, |call, cx| {
911 call.invite(client_c.user_id().unwrap(), Some(project_a.clone()), cx)
912 })
913 .await
914 .unwrap();
915 active_call_a
916 .update(cx_a, |call, cx| {
917 call.invite(client_d.user_id().unwrap(), Some(project_a.clone()), cx)
918 })
919 .await
920 .unwrap();
921
922 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
923
924 // User B receives the call and joins the room.
925
926 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
927 assert!(incoming_call_b.next().await.unwrap().is_some());
928 active_call_b
929 .update(cx_b, |call, cx| call.accept_incoming(cx))
930 .await
931 .unwrap();
932
933 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
934
935 // User C receives the call and joins the room.
936
937 let mut incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
938 assert!(incoming_call_c.next().await.unwrap().is_some());
939 active_call_c
940 .update(cx_c, |call, cx| call.accept_incoming(cx))
941 .await
942 .unwrap();
943
944 let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
945
946 // User D receives the call but doesn't join the room yet.
947
948 let mut incoming_call_d = active_call_d.read_with(cx_d, |call, _| call.incoming());
949 assert!(incoming_call_d.next().await.unwrap().is_some());
950
951 executor.run_until_parked();
952 assert_eq!(
953 room_participants(&room_a, cx_a),
954 RoomParticipants {
955 remote: vec!["user_b".to_string(), "user_c".to_string()],
956 pending: vec!["user_d".to_string()]
957 }
958 );
959 assert_eq!(
960 room_participants(&room_b, cx_b),
961 RoomParticipants {
962 remote: vec!["user_a".to_string(), "user_c".to_string()],
963 pending: vec!["user_d".to_string()]
964 }
965 );
966 assert_eq!(
967 room_participants(&room_c, cx_c),
968 RoomParticipants {
969 remote: vec!["user_a".to_string(), "user_b".to_string()],
970 pending: vec!["user_d".to_string()]
971 }
972 );
973
974 // The server is torn down.
975 server.reset().await;
976
977 // Users A and B reconnect to the call. User C has troubles reconnecting, so it leaves the room.
978 client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
979 executor.advance_clock(RECONNECT_TIMEOUT);
980 assert_eq!(
981 room_participants(&room_a, cx_a),
982 RoomParticipants {
983 remote: vec!["user_b".to_string(), "user_c".to_string()],
984 pending: vec!["user_d".to_string()]
985 }
986 );
987 assert_eq!(
988 room_participants(&room_b, cx_b),
989 RoomParticipants {
990 remote: vec!["user_a".to_string(), "user_c".to_string()],
991 pending: vec!["user_d".to_string()]
992 }
993 );
994 assert_eq!(
995 room_participants(&room_c, cx_c),
996 RoomParticipants {
997 remote: vec![],
998 pending: vec![]
999 }
1000 );
1001
1002 // User D is notified again of the incoming call and accepts it.
1003 assert!(incoming_call_d.next().await.unwrap().is_some());
1004 active_call_d
1005 .update(cx_d, |call, cx| call.accept_incoming(cx))
1006 .await
1007 .unwrap();
1008 executor.run_until_parked();
1009
1010 let room_d = active_call_d.read_with(cx_d, |call, _| call.room().unwrap().clone());
1011 assert_eq!(
1012 room_participants(&room_a, cx_a),
1013 RoomParticipants {
1014 remote: vec![
1015 "user_b".to_string(),
1016 "user_c".to_string(),
1017 "user_d".to_string(),
1018 ],
1019 pending: vec![]
1020 }
1021 );
1022 assert_eq!(
1023 room_participants(&room_b, cx_b),
1024 RoomParticipants {
1025 remote: vec![
1026 "user_a".to_string(),
1027 "user_c".to_string(),
1028 "user_d".to_string(),
1029 ],
1030 pending: vec![]
1031 }
1032 );
1033 assert_eq!(
1034 room_participants(&room_c, cx_c),
1035 RoomParticipants {
1036 remote: vec![],
1037 pending: vec![]
1038 }
1039 );
1040 assert_eq!(
1041 room_participants(&room_d, cx_d),
1042 RoomParticipants {
1043 remote: vec![
1044 "user_a".to_string(),
1045 "user_b".to_string(),
1046 "user_c".to_string(),
1047 ],
1048 pending: vec![]
1049 }
1050 );
1051
1052 // The server finishes restarting, cleaning up stale connections.
1053 server.start().await.unwrap();
1054 executor.advance_clock(CLEANUP_TIMEOUT);
1055 assert_eq!(
1056 room_participants(&room_a, cx_a),
1057 RoomParticipants {
1058 remote: vec!["user_b".to_string(), "user_d".to_string()],
1059 pending: vec![]
1060 }
1061 );
1062 assert_eq!(
1063 room_participants(&room_b, cx_b),
1064 RoomParticipants {
1065 remote: vec!["user_a".to_string(), "user_d".to_string()],
1066 pending: vec![]
1067 }
1068 );
1069 assert_eq!(
1070 room_participants(&room_c, cx_c),
1071 RoomParticipants {
1072 remote: vec![],
1073 pending: vec![]
1074 }
1075 );
1076 assert_eq!(
1077 room_participants(&room_d, cx_d),
1078 RoomParticipants {
1079 remote: vec!["user_a".to_string(), "user_b".to_string()],
1080 pending: vec![]
1081 }
1082 );
1083
1084 // User D hangs up.
1085 active_call_d
1086 .update(cx_d, |call, cx| call.hang_up(cx))
1087 .await
1088 .unwrap();
1089 executor.run_until_parked();
1090 assert_eq!(
1091 room_participants(&room_a, cx_a),
1092 RoomParticipants {
1093 remote: vec!["user_b".to_string()],
1094 pending: vec![]
1095 }
1096 );
1097 assert_eq!(
1098 room_participants(&room_b, cx_b),
1099 RoomParticipants {
1100 remote: vec!["user_a".to_string()],
1101 pending: vec![]
1102 }
1103 );
1104 assert_eq!(
1105 room_participants(&room_c, cx_c),
1106 RoomParticipants {
1107 remote: vec![],
1108 pending: vec![]
1109 }
1110 );
1111 assert_eq!(
1112 room_participants(&room_d, cx_d),
1113 RoomParticipants {
1114 remote: vec![],
1115 pending: vec![]
1116 }
1117 );
1118
1119 // User B calls user D again.
1120 active_call_b
1121 .update(cx_b, |call, cx| {
1122 call.invite(client_d.user_id().unwrap(), None, cx)
1123 })
1124 .await
1125 .unwrap();
1126
1127 // User D receives the call but doesn't join the room yet.
1128
1129 let mut incoming_call_d = active_call_d.read_with(cx_d, |call, _| call.incoming());
1130 assert!(incoming_call_d.next().await.unwrap().is_some());
1131 executor.run_until_parked();
1132 assert_eq!(
1133 room_participants(&room_a, cx_a),
1134 RoomParticipants {
1135 remote: vec!["user_b".to_string()],
1136 pending: vec!["user_d".to_string()]
1137 }
1138 );
1139 assert_eq!(
1140 room_participants(&room_b, cx_b),
1141 RoomParticipants {
1142 remote: vec!["user_a".to_string()],
1143 pending: vec!["user_d".to_string()]
1144 }
1145 );
1146
1147 // The server is torn down.
1148 server.reset().await;
1149
1150 // Users A and B have troubles reconnecting, so they leave the room.
1151 client_a.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
1152 client_b.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
1153 client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
1154 executor.advance_clock(RECONNECT_TIMEOUT);
1155 assert_eq!(
1156 room_participants(&room_a, cx_a),
1157 RoomParticipants {
1158 remote: vec![],
1159 pending: vec![]
1160 }
1161 );
1162 assert_eq!(
1163 room_participants(&room_b, cx_b),
1164 RoomParticipants {
1165 remote: vec![],
1166 pending: vec![]
1167 }
1168 );
1169
1170 // User D is notified again of the incoming call but doesn't accept it.
1171 assert!(incoming_call_d.next().await.unwrap().is_some());
1172
1173 // The server finishes restarting, cleaning up stale connections and canceling the
1174 // call to user D because the room has become empty.
1175 server.start().await.unwrap();
1176 executor.advance_clock(CLEANUP_TIMEOUT);
1177 assert!(incoming_call_d.next().await.unwrap().is_none());
1178}
1179
1180#[gpui::test(iterations = 10)]
1181async fn test_calls_on_multiple_connections(
1182 executor: BackgroundExecutor,
1183 cx_a: &mut TestAppContext,
1184 cx_b1: &mut TestAppContext,
1185 cx_b2: &mut TestAppContext,
1186) {
1187 let mut server = TestServer::start(executor.clone()).await;
1188 let client_a = server.create_client(cx_a, "user_a").await;
1189 let client_b1 = server.create_client(cx_b1, "user_b").await;
1190 let client_b2 = server.create_client(cx_b2, "user_b").await;
1191 server
1192 .make_contacts(&mut [(&client_a, cx_a), (&client_b1, cx_b1)])
1193 .await;
1194
1195 let active_call_a = cx_a.read(ActiveCall::global);
1196 let active_call_b1 = cx_b1.read(ActiveCall::global);
1197 let active_call_b2 = cx_b2.read(ActiveCall::global);
1198
1199 let mut incoming_call_b1 = active_call_b1.read_with(cx_b1, |call, _| call.incoming());
1200
1201 let mut incoming_call_b2 = active_call_b2.read_with(cx_b2, |call, _| call.incoming());
1202 assert!(incoming_call_b1.next().await.unwrap().is_none());
1203 assert!(incoming_call_b2.next().await.unwrap().is_none());
1204
1205 // Call user B from client A, ensuring both clients for user B ring.
1206 active_call_a
1207 .update(cx_a, |call, cx| {
1208 call.invite(client_b1.user_id().unwrap(), None, cx)
1209 })
1210 .await
1211 .unwrap();
1212 executor.run_until_parked();
1213 assert!(incoming_call_b1.next().await.unwrap().is_some());
1214 assert!(incoming_call_b2.next().await.unwrap().is_some());
1215
1216 // User B declines the call on one of the two connections, causing both connections
1217 // to stop ringing.
1218 active_call_b2.update(cx_b2, |call, cx| call.decline_incoming(cx).unwrap());
1219 executor.run_until_parked();
1220 assert!(incoming_call_b1.next().await.unwrap().is_none());
1221 assert!(incoming_call_b2.next().await.unwrap().is_none());
1222
1223 // Call user B again from client A.
1224 active_call_a
1225 .update(cx_a, |call, cx| {
1226 call.invite(client_b1.user_id().unwrap(), None, cx)
1227 })
1228 .await
1229 .unwrap();
1230 executor.run_until_parked();
1231 assert!(incoming_call_b1.next().await.unwrap().is_some());
1232 assert!(incoming_call_b2.next().await.unwrap().is_some());
1233
1234 // User B accepts the call on one of the two connections, causing both connections
1235 // to stop ringing.
1236 active_call_b2
1237 .update(cx_b2, |call, cx| call.accept_incoming(cx))
1238 .await
1239 .unwrap();
1240 executor.run_until_parked();
1241 assert!(incoming_call_b1.next().await.unwrap().is_none());
1242 assert!(incoming_call_b2.next().await.unwrap().is_none());
1243
1244 // User B disconnects the client that is not on the call. Everything should be fine.
1245 client_b1.disconnect(&cx_b1.to_async());
1246 executor.advance_clock(RECEIVE_TIMEOUT);
1247 client_b1
1248 .authenticate_and_connect(false, &cx_b1.to_async())
1249 .await
1250 .unwrap();
1251
1252 // User B hangs up, and user A calls them again.
1253 active_call_b2
1254 .update(cx_b2, |call, cx| call.hang_up(cx))
1255 .await
1256 .unwrap();
1257 executor.run_until_parked();
1258 active_call_a
1259 .update(cx_a, |call, cx| {
1260 call.invite(client_b1.user_id().unwrap(), None, cx)
1261 })
1262 .await
1263 .unwrap();
1264 executor.run_until_parked();
1265 assert!(incoming_call_b1.next().await.unwrap().is_some());
1266 assert!(incoming_call_b2.next().await.unwrap().is_some());
1267
1268 // User A cancels the call, causing both connections to stop ringing.
1269 active_call_a
1270 .update(cx_a, |call, cx| {
1271 call.cancel_invite(client_b1.user_id().unwrap(), cx)
1272 })
1273 .await
1274 .unwrap();
1275 executor.run_until_parked();
1276 assert!(incoming_call_b1.next().await.unwrap().is_none());
1277 assert!(incoming_call_b2.next().await.unwrap().is_none());
1278
1279 // User A calls user B again.
1280 active_call_a
1281 .update(cx_a, |call, cx| {
1282 call.invite(client_b1.user_id().unwrap(), None, cx)
1283 })
1284 .await
1285 .unwrap();
1286 executor.run_until_parked();
1287 assert!(incoming_call_b1.next().await.unwrap().is_some());
1288 assert!(incoming_call_b2.next().await.unwrap().is_some());
1289
1290 // User A hangs up, causing both connections to stop ringing.
1291 active_call_a
1292 .update(cx_a, |call, cx| call.hang_up(cx))
1293 .await
1294 .unwrap();
1295 executor.run_until_parked();
1296 assert!(incoming_call_b1.next().await.unwrap().is_none());
1297 assert!(incoming_call_b2.next().await.unwrap().is_none());
1298
1299 // User A calls user B again.
1300 active_call_a
1301 .update(cx_a, |call, cx| {
1302 call.invite(client_b1.user_id().unwrap(), None, cx)
1303 })
1304 .await
1305 .unwrap();
1306 executor.run_until_parked();
1307 assert!(incoming_call_b1.next().await.unwrap().is_some());
1308 assert!(incoming_call_b2.next().await.unwrap().is_some());
1309
1310 // User A disconnects, causing both connections to stop ringing.
1311 server.forbid_connections();
1312 server.disconnect_client(client_a.peer_id().unwrap());
1313 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
1314 assert!(incoming_call_b1.next().await.unwrap().is_none());
1315 assert!(incoming_call_b2.next().await.unwrap().is_none());
1316
1317 // User A reconnects automatically, then calls user B again.
1318 server.allow_connections();
1319 executor.advance_clock(RECEIVE_TIMEOUT);
1320 active_call_a
1321 .update(cx_a, |call, cx| {
1322 call.invite(client_b1.user_id().unwrap(), None, cx)
1323 })
1324 .await
1325 .unwrap();
1326 executor.run_until_parked();
1327 assert!(incoming_call_b1.next().await.unwrap().is_some());
1328 assert!(incoming_call_b2.next().await.unwrap().is_some());
1329
1330 // User B disconnects all clients, causing user A to no longer see a pending call for them.
1331 server.forbid_connections();
1332 server.disconnect_client(client_b1.peer_id().unwrap());
1333 server.disconnect_client(client_b2.peer_id().unwrap());
1334 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
1335
1336 active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none()));
1337}
1338
1339#[gpui::test(iterations = 10)]
1340async fn test_unshare_project(
1341 executor: BackgroundExecutor,
1342 cx_a: &mut TestAppContext,
1343 cx_b: &mut TestAppContext,
1344 cx_c: &mut TestAppContext,
1345) {
1346 let mut server = TestServer::start(executor.clone()).await;
1347 let client_a = server.create_client(cx_a, "user_a").await;
1348 let client_b = server.create_client(cx_b, "user_b").await;
1349 let client_c = server.create_client(cx_c, "user_c").await;
1350 server
1351 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1352 .await;
1353
1354 let active_call_a = cx_a.read(ActiveCall::global);
1355 let active_call_b = cx_b.read(ActiveCall::global);
1356
1357 client_a
1358 .fs()
1359 .insert_tree(
1360 "/a",
1361 json!({
1362 "a.txt": "a-contents",
1363 "b.txt": "b-contents",
1364 }),
1365 )
1366 .await;
1367
1368 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1369 let project_id = active_call_a
1370 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1371 .await
1372 .unwrap();
1373
1374 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
1375 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1376 executor.run_until_parked();
1377
1378 assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
1379
1380 project_b
1381 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
1382 .await
1383 .unwrap();
1384
1385 // When client B leaves the room, the project becomes read-only.
1386 active_call_b
1387 .update(cx_b, |call, cx| call.hang_up(cx))
1388 .await
1389 .unwrap();
1390 executor.run_until_parked();
1391
1392 assert!(project_b.read_with(cx_b, |project, cx| project.is_disconnected(cx)));
1393
1394 // Client C opens the project.
1395 let project_c = client_c.join_remote_project(project_id, cx_c).await;
1396
1397 // When client A unshares the project, client C's project becomes read-only.
1398 project_a
1399 .update(cx_a, |project, cx| project.unshare(cx))
1400 .unwrap();
1401 executor.run_until_parked();
1402
1403 assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
1404
1405 assert!(project_c.read_with(cx_c, |project, cx| project.is_disconnected(cx)));
1406
1407 // Client C can open the project again after client A re-shares.
1408 let project_id = active_call_a
1409 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1410 .await
1411 .unwrap();
1412 let project_c2 = client_c.join_remote_project(project_id, cx_c).await;
1413 executor.run_until_parked();
1414
1415 assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
1416 project_c2
1417 .update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
1418 .await
1419 .unwrap();
1420
1421 // When client A (the host) leaves the room, the project gets unshared and guests are notified.
1422 active_call_a
1423 .update(cx_a, |call, cx| call.hang_up(cx))
1424 .await
1425 .unwrap();
1426 executor.run_until_parked();
1427
1428 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
1429
1430 project_c2.read_with(cx_c, |project, cx| {
1431 assert!(project.is_disconnected(cx));
1432 assert!(project.collaborators().is_empty());
1433 });
1434}
1435
1436#[gpui::test(iterations = 10)]
1437async fn test_project_reconnect(
1438 executor: BackgroundExecutor,
1439 cx_a: &mut TestAppContext,
1440 cx_b: &mut TestAppContext,
1441) {
1442 let mut server = TestServer::start(executor.clone()).await;
1443 let client_a = server.create_client(cx_a, "user_a").await;
1444 let client_b = server.create_client(cx_b, "user_b").await;
1445 server
1446 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1447 .await;
1448
1449 cx_b.update(editor::init);
1450
1451 client_a
1452 .fs()
1453 .insert_tree(
1454 "/root-1",
1455 json!({
1456 "dir1": {
1457 "a.txt": "a",
1458 "b.txt": "b",
1459 "subdir1": {
1460 "c.txt": "c",
1461 "d.txt": "d",
1462 "e.txt": "e",
1463 }
1464 },
1465 "dir2": {
1466 "v.txt": "v",
1467 },
1468 "dir3": {
1469 "w.txt": "w",
1470 "x.txt": "x",
1471 "y.txt": "y",
1472 },
1473 "dir4": {
1474 "z.txt": "z",
1475 },
1476 }),
1477 )
1478 .await;
1479 client_a
1480 .fs()
1481 .insert_tree(
1482 "/root-2",
1483 json!({
1484 "2.txt": "2",
1485 }),
1486 )
1487 .await;
1488 client_a
1489 .fs()
1490 .insert_tree(
1491 "/root-3",
1492 json!({
1493 "3.txt": "3",
1494 }),
1495 )
1496 .await;
1497
1498 let active_call_a = cx_a.read(ActiveCall::global);
1499 let (project_a1, _) = client_a.build_local_project("/root-1/dir1", cx_a).await;
1500 let (project_a2, _) = client_a.build_local_project("/root-2", cx_a).await;
1501 let (project_a3, _) = client_a.build_local_project("/root-3", cx_a).await;
1502 let worktree_a1 =
1503 project_a1.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
1504 let project1_id = active_call_a
1505 .update(cx_a, |call, cx| call.share_project(project_a1.clone(), cx))
1506 .await
1507 .unwrap();
1508 let project2_id = active_call_a
1509 .update(cx_a, |call, cx| call.share_project(project_a2.clone(), cx))
1510 .await
1511 .unwrap();
1512 let project3_id = active_call_a
1513 .update(cx_a, |call, cx| call.share_project(project_a3.clone(), cx))
1514 .await
1515 .unwrap();
1516
1517 let project_b1 = client_b.join_remote_project(project1_id, cx_b).await;
1518 let project_b2 = client_b.join_remote_project(project2_id, cx_b).await;
1519 let project_b3 = client_b.join_remote_project(project3_id, cx_b).await;
1520 executor.run_until_parked();
1521
1522 let worktree1_id = worktree_a1.read_with(cx_a, |worktree, _| {
1523 assert!(worktree.has_update_observer());
1524 worktree.id()
1525 });
1526 let (worktree_a2, _) = project_a1
1527 .update(cx_a, |p, cx| {
1528 p.find_or_create_worktree("/root-1/dir2", true, cx)
1529 })
1530 .await
1531 .unwrap();
1532 executor.run_until_parked();
1533
1534 let worktree2_id = worktree_a2.read_with(cx_a, |tree, _| {
1535 assert!(tree.has_update_observer());
1536 tree.id()
1537 });
1538 executor.run_until_parked();
1539
1540 project_b1.read_with(cx_b, |project, cx| {
1541 assert!(project.worktree_for_id(worktree2_id, cx).is_some())
1542 });
1543
1544 let buffer_a1 = project_a1
1545 .update(cx_a, |p, cx| p.open_buffer((worktree1_id, "a.txt"), cx))
1546 .await
1547 .unwrap();
1548 let buffer_b1 = project_b1
1549 .update(cx_b, |p, cx| p.open_buffer((worktree1_id, "a.txt"), cx))
1550 .await
1551 .unwrap();
1552
1553 // Drop client A's connection.
1554 server.forbid_connections();
1555 server.disconnect_client(client_a.peer_id().unwrap());
1556 executor.advance_clock(RECEIVE_TIMEOUT);
1557
1558 project_a1.read_with(cx_a, |project, _| {
1559 assert!(project.is_shared());
1560 assert_eq!(project.collaborators().len(), 1);
1561 });
1562
1563 project_b1.read_with(cx_b, |project, cx| {
1564 assert!(!project.is_disconnected(cx));
1565 assert_eq!(project.collaborators().len(), 1);
1566 });
1567
1568 worktree_a1.read_with(cx_a, |tree, _| assert!(tree.has_update_observer()));
1569
1570 // While client A is disconnected, add and remove files from client A's project.
1571 client_a
1572 .fs()
1573 .insert_tree(
1574 "/root-1/dir1/subdir2",
1575 json!({
1576 "f.txt": "f-contents",
1577 "g.txt": "g-contents",
1578 "h.txt": "h-contents",
1579 "i.txt": "i-contents",
1580 }),
1581 )
1582 .await;
1583 client_a
1584 .fs()
1585 .remove_dir(
1586 "/root-1/dir1/subdir1".as_ref(),
1587 RemoveOptions {
1588 recursive: true,
1589 ..Default::default()
1590 },
1591 )
1592 .await
1593 .unwrap();
1594
1595 // While client A is disconnected, add and remove worktrees from client A's project.
1596 project_a1.update(cx_a, |project, cx| {
1597 project.remove_worktree(worktree2_id, cx)
1598 });
1599 let (worktree_a3, _) = project_a1
1600 .update(cx_a, |p, cx| {
1601 p.find_or_create_worktree("/root-1/dir3", true, cx)
1602 })
1603 .await
1604 .unwrap();
1605 worktree_a3
1606 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
1607 .await;
1608
1609 let worktree3_id = worktree_a3.read_with(cx_a, |tree, _| {
1610 assert!(!tree.has_update_observer());
1611 tree.id()
1612 });
1613 executor.run_until_parked();
1614
1615 // While client A is disconnected, close project 2
1616 cx_a.update(|_| drop(project_a2));
1617
1618 // While client A is disconnected, mutate a buffer on both the host and the guest.
1619 buffer_a1.update(cx_a, |buf, cx| buf.edit([(0..0, "W")], None, cx));
1620 buffer_b1.update(cx_b, |buf, cx| buf.edit([(1..1, "Z")], None, cx));
1621 executor.run_until_parked();
1622
1623 // Client A reconnects. Their project is re-shared, and client B re-joins it.
1624 server.allow_connections();
1625 client_a
1626 .authenticate_and_connect(false, &cx_a.to_async())
1627 .await
1628 .unwrap();
1629 executor.run_until_parked();
1630
1631 project_a1.read_with(cx_a, |project, cx| {
1632 assert!(project.is_shared());
1633 assert!(worktree_a1.read(cx).has_update_observer());
1634 assert_eq!(
1635 worktree_a1
1636 .read(cx)
1637 .snapshot()
1638 .paths()
1639 .map(|p| p.to_str().unwrap())
1640 .collect::<Vec<_>>(),
1641 vec![
1642 "a.txt",
1643 "b.txt",
1644 "subdir2",
1645 "subdir2/f.txt",
1646 "subdir2/g.txt",
1647 "subdir2/h.txt",
1648 "subdir2/i.txt"
1649 ]
1650 );
1651 assert!(worktree_a3.read(cx).has_update_observer());
1652 assert_eq!(
1653 worktree_a3
1654 .read(cx)
1655 .snapshot()
1656 .paths()
1657 .map(|p| p.to_str().unwrap())
1658 .collect::<Vec<_>>(),
1659 vec!["w.txt", "x.txt", "y.txt"]
1660 );
1661 });
1662
1663 project_b1.read_with(cx_b, |project, cx| {
1664 assert!(!project.is_disconnected(cx));
1665 assert_eq!(
1666 project
1667 .worktree_for_id(worktree1_id, cx)
1668 .unwrap()
1669 .read(cx)
1670 .snapshot()
1671 .paths()
1672 .map(|p| p.to_str().unwrap())
1673 .collect::<Vec<_>>(),
1674 vec![
1675 "a.txt",
1676 "b.txt",
1677 "subdir2",
1678 "subdir2/f.txt",
1679 "subdir2/g.txt",
1680 "subdir2/h.txt",
1681 "subdir2/i.txt"
1682 ]
1683 );
1684 assert!(project.worktree_for_id(worktree2_id, cx).is_none());
1685 assert_eq!(
1686 project
1687 .worktree_for_id(worktree3_id, cx)
1688 .unwrap()
1689 .read(cx)
1690 .snapshot()
1691 .paths()
1692 .map(|p| p.to_str().unwrap())
1693 .collect::<Vec<_>>(),
1694 vec!["w.txt", "x.txt", "y.txt"]
1695 );
1696 });
1697
1698 project_b2.read_with(cx_b, |project, cx| assert!(project.is_disconnected(cx)));
1699
1700 project_b3.read_with(cx_b, |project, cx| assert!(!project.is_disconnected(cx)));
1701
1702 buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WaZ"));
1703
1704 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "WaZ"));
1705
1706 // Drop client B's connection.
1707 server.forbid_connections();
1708 server.disconnect_client(client_b.peer_id().unwrap());
1709 executor.advance_clock(RECEIVE_TIMEOUT);
1710
1711 // While client B is disconnected, add and remove files from client A's project
1712 client_a
1713 .fs()
1714 .insert_file("/root-1/dir1/subdir2/j.txt", "j-contents".into())
1715 .await;
1716 client_a
1717 .fs()
1718 .remove_file("/root-1/dir1/subdir2/i.txt".as_ref(), Default::default())
1719 .await
1720 .unwrap();
1721
1722 // While client B is disconnected, add and remove worktrees from client A's project.
1723 let (worktree_a4, _) = project_a1
1724 .update(cx_a, |p, cx| {
1725 p.find_or_create_worktree("/root-1/dir4", true, cx)
1726 })
1727 .await
1728 .unwrap();
1729 executor.run_until_parked();
1730
1731 let worktree4_id = worktree_a4.read_with(cx_a, |tree, _| {
1732 assert!(tree.has_update_observer());
1733 tree.id()
1734 });
1735 project_a1.update(cx_a, |project, cx| {
1736 project.remove_worktree(worktree3_id, cx)
1737 });
1738 executor.run_until_parked();
1739
1740 // While client B is disconnected, mutate a buffer on both the host and the guest.
1741 buffer_a1.update(cx_a, |buf, cx| buf.edit([(1..1, "X")], None, cx));
1742 buffer_b1.update(cx_b, |buf, cx| buf.edit([(2..2, "Y")], None, cx));
1743 executor.run_until_parked();
1744
1745 // While disconnected, close project 3
1746 cx_a.update(|_| drop(project_a3));
1747
1748 // Client B reconnects. They re-join the room and the remaining shared project.
1749 server.allow_connections();
1750 client_b
1751 .authenticate_and_connect(false, &cx_b.to_async())
1752 .await
1753 .unwrap();
1754 executor.run_until_parked();
1755
1756 project_b1.read_with(cx_b, |project, cx| {
1757 assert!(!project.is_disconnected(cx));
1758 assert_eq!(
1759 project
1760 .worktree_for_id(worktree1_id, cx)
1761 .unwrap()
1762 .read(cx)
1763 .snapshot()
1764 .paths()
1765 .map(|p| p.to_str().unwrap())
1766 .collect::<Vec<_>>(),
1767 vec![
1768 "a.txt",
1769 "b.txt",
1770 "subdir2",
1771 "subdir2/f.txt",
1772 "subdir2/g.txt",
1773 "subdir2/h.txt",
1774 "subdir2/j.txt"
1775 ]
1776 );
1777 assert!(project.worktree_for_id(worktree2_id, cx).is_none());
1778 assert_eq!(
1779 project
1780 .worktree_for_id(worktree4_id, cx)
1781 .unwrap()
1782 .read(cx)
1783 .snapshot()
1784 .paths()
1785 .map(|p| p.to_str().unwrap())
1786 .collect::<Vec<_>>(),
1787 vec!["z.txt"]
1788 );
1789 });
1790
1791 project_b3.read_with(cx_b, |project, cx| assert!(project.is_disconnected(cx)));
1792
1793 buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WXaYZ"));
1794
1795 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "WXaYZ"));
1796}
1797
1798#[gpui::test(iterations = 10)]
1799async fn test_active_call_events(
1800 executor: BackgroundExecutor,
1801 cx_a: &mut TestAppContext,
1802 cx_b: &mut TestAppContext,
1803) {
1804 let mut server = TestServer::start(executor.clone()).await;
1805 let client_a = server.create_client(cx_a, "user_a").await;
1806 let client_b = server.create_client(cx_b, "user_b").await;
1807 client_a.fs().insert_tree("/a", json!({})).await;
1808 client_b.fs().insert_tree("/b", json!({})).await;
1809
1810 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
1811 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
1812
1813 server
1814 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1815 .await;
1816 let active_call_a = cx_a.read(ActiveCall::global);
1817 let active_call_b = cx_b.read(ActiveCall::global);
1818
1819 let events_a = active_call_events(cx_a);
1820 let events_b = active_call_events(cx_b);
1821
1822 let project_a_id = active_call_a
1823 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1824 .await
1825 .unwrap();
1826 executor.run_until_parked();
1827 assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]);
1828 assert_eq!(
1829 mem::take(&mut *events_b.borrow_mut()),
1830 vec![room::Event::RemoteProjectShared {
1831 owner: Arc::new(User {
1832 id: client_a.user_id().unwrap(),
1833 github_login: "user_a".to_string(),
1834 avatar_uri: "avatar_a".into(),
1835 }),
1836 project_id: project_a_id,
1837 worktree_root_names: vec!["a".to_string()],
1838 }]
1839 );
1840
1841 let project_b_id = active_call_b
1842 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1843 .await
1844 .unwrap();
1845 executor.run_until_parked();
1846 assert_eq!(
1847 mem::take(&mut *events_a.borrow_mut()),
1848 vec![room::Event::RemoteProjectShared {
1849 owner: Arc::new(User {
1850 id: client_b.user_id().unwrap(),
1851 github_login: "user_b".to_string(),
1852 avatar_uri: "avatar_b".into(),
1853 }),
1854 project_id: project_b_id,
1855 worktree_root_names: vec!["b".to_string()]
1856 }]
1857 );
1858 assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]);
1859
1860 // Sharing a project twice is idempotent.
1861 let project_b_id_2 = active_call_b
1862 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1863 .await
1864 .unwrap();
1865 assert_eq!(project_b_id_2, project_b_id);
1866 executor.run_until_parked();
1867 assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]);
1868 assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]);
1869
1870 // Unsharing a project should dispatch the RemoteProjectUnshared event.
1871 active_call_a
1872 .update(cx_a, |call, cx| call.hang_up(cx))
1873 .await
1874 .unwrap();
1875 executor.run_until_parked();
1876
1877 assert_eq!(
1878 mem::take(&mut *events_a.borrow_mut()),
1879 vec![room::Event::RoomLeft { channel_id: None }]
1880 );
1881 assert_eq!(
1882 mem::take(&mut *events_b.borrow_mut()),
1883 vec![room::Event::RemoteProjectUnshared {
1884 project_id: project_a_id,
1885 }]
1886 );
1887}
1888
1889fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>> {
1890 let events = Rc::new(RefCell::new(Vec::new()));
1891 let active_call = cx.read(ActiveCall::global);
1892 cx.update({
1893 let events = events.clone();
1894 |cx| {
1895 cx.subscribe(&active_call, move |_, event, _| {
1896 events.borrow_mut().push(event.clone())
1897 })
1898 .detach()
1899 }
1900 });
1901 events
1902}
1903
1904#[gpui::test]
1905async fn test_mute_deafen(
1906 executor: BackgroundExecutor,
1907 cx_a: &mut TestAppContext,
1908 cx_b: &mut TestAppContext,
1909 cx_c: &mut TestAppContext,
1910) {
1911 let mut server = TestServer::start(executor.clone()).await;
1912 let client_a = server.create_client(cx_a, "user_a").await;
1913 let client_b = server.create_client(cx_b, "user_b").await;
1914 let client_c = server.create_client(cx_c, "user_c").await;
1915
1916 server
1917 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1918 .await;
1919
1920 let active_call_a = cx_a.read(ActiveCall::global);
1921 let active_call_b = cx_b.read(ActiveCall::global);
1922 let active_call_c = cx_c.read(ActiveCall::global);
1923
1924 // User A calls user B, B answers.
1925 active_call_a
1926 .update(cx_a, |call, cx| {
1927 call.invite(client_b.user_id().unwrap(), None, cx)
1928 })
1929 .await
1930 .unwrap();
1931 executor.run_until_parked();
1932 active_call_b
1933 .update(cx_b, |call, cx| call.accept_incoming(cx))
1934 .await
1935 .unwrap();
1936 executor.run_until_parked();
1937
1938 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
1939 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
1940
1941 room_a.read_with(cx_a, |room, _| assert!(!room.is_muted()));
1942 room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
1943
1944 // Users A and B are both unmuted.
1945 assert_eq!(
1946 participant_audio_state(&room_a, cx_a),
1947 &[ParticipantAudioState {
1948 user_id: client_b.user_id().unwrap(),
1949 is_muted: false,
1950 audio_tracks_playing: vec![true],
1951 }]
1952 );
1953 assert_eq!(
1954 participant_audio_state(&room_b, cx_b),
1955 &[ParticipantAudioState {
1956 user_id: client_a.user_id().unwrap(),
1957 is_muted: false,
1958 audio_tracks_playing: vec![true],
1959 }]
1960 );
1961
1962 // User A mutes
1963 room_a.update(cx_a, |room, cx| room.toggle_mute(cx));
1964 executor.run_until_parked();
1965
1966 // User A hears user B, but B doesn't hear A.
1967 room_a.read_with(cx_a, |room, _| assert!(room.is_muted()));
1968 room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
1969 assert_eq!(
1970 participant_audio_state(&room_a, cx_a),
1971 &[ParticipantAudioState {
1972 user_id: client_b.user_id().unwrap(),
1973 is_muted: false,
1974 audio_tracks_playing: vec![true],
1975 }]
1976 );
1977 assert_eq!(
1978 participant_audio_state(&room_b, cx_b),
1979 &[ParticipantAudioState {
1980 user_id: client_a.user_id().unwrap(),
1981 is_muted: true,
1982 audio_tracks_playing: vec![true],
1983 }]
1984 );
1985
1986 // User A deafens
1987 room_a.update(cx_a, |room, cx| room.toggle_deafen(cx));
1988 executor.run_until_parked();
1989
1990 // User A does not hear user B.
1991 room_a.read_with(cx_a, |room, _| assert!(room.is_muted()));
1992 room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
1993 assert_eq!(
1994 participant_audio_state(&room_a, cx_a),
1995 &[ParticipantAudioState {
1996 user_id: client_b.user_id().unwrap(),
1997 is_muted: false,
1998 audio_tracks_playing: vec![false],
1999 }]
2000 );
2001 assert_eq!(
2002 participant_audio_state(&room_b, cx_b),
2003 &[ParticipantAudioState {
2004 user_id: client_a.user_id().unwrap(),
2005 is_muted: true,
2006 audio_tracks_playing: vec![true],
2007 }]
2008 );
2009
2010 // User B calls user C, C joins.
2011 active_call_b
2012 .update(cx_b, |call, cx| {
2013 call.invite(client_c.user_id().unwrap(), None, cx)
2014 })
2015 .await
2016 .unwrap();
2017 executor.run_until_parked();
2018 active_call_c
2019 .update(cx_c, |call, cx| call.accept_incoming(cx))
2020 .await
2021 .unwrap();
2022 executor.run_until_parked();
2023
2024 // User A does not hear users B or C.
2025 assert_eq!(
2026 participant_audio_state(&room_a, cx_a),
2027 &[
2028 ParticipantAudioState {
2029 user_id: client_b.user_id().unwrap(),
2030 is_muted: false,
2031 audio_tracks_playing: vec![false],
2032 },
2033 ParticipantAudioState {
2034 user_id: client_c.user_id().unwrap(),
2035 is_muted: false,
2036 audio_tracks_playing: vec![false],
2037 }
2038 ]
2039 );
2040 assert_eq!(
2041 participant_audio_state(&room_b, cx_b),
2042 &[
2043 ParticipantAudioState {
2044 user_id: client_a.user_id().unwrap(),
2045 is_muted: true,
2046 audio_tracks_playing: vec![true],
2047 },
2048 ParticipantAudioState {
2049 user_id: client_c.user_id().unwrap(),
2050 is_muted: false,
2051 audio_tracks_playing: vec![true],
2052 }
2053 ]
2054 );
2055
2056 #[derive(PartialEq, Eq, Debug)]
2057 struct ParticipantAudioState {
2058 user_id: u64,
2059 is_muted: bool,
2060 audio_tracks_playing: Vec<bool>,
2061 }
2062
2063 fn participant_audio_state(
2064 room: &Model<Room>,
2065 cx: &TestAppContext,
2066 ) -> Vec<ParticipantAudioState> {
2067 room.read_with(cx, |room, _| {
2068 room.remote_participants()
2069 .iter()
2070 .map(|(user_id, participant)| ParticipantAudioState {
2071 user_id: *user_id,
2072 is_muted: participant.muted,
2073 audio_tracks_playing: participant
2074 .audio_tracks
2075 .values()
2076 .map(|(track, _)| track.rtc_track().enabled())
2077 .collect(),
2078 })
2079 .collect::<Vec<_>>()
2080 })
2081 }
2082}
2083
2084#[gpui::test(iterations = 10)]
2085async fn test_room_location(
2086 executor: BackgroundExecutor,
2087 cx_a: &mut TestAppContext,
2088 cx_b: &mut TestAppContext,
2089) {
2090 let mut server = TestServer::start(executor.clone()).await;
2091 let client_a = server.create_client(cx_a, "user_a").await;
2092 let client_b = server.create_client(cx_b, "user_b").await;
2093 client_a.fs().insert_tree("/a", json!({})).await;
2094 client_b.fs().insert_tree("/b", json!({})).await;
2095
2096 let active_call_a = cx_a.read(ActiveCall::global);
2097 let active_call_b = cx_b.read(ActiveCall::global);
2098
2099 let a_notified = Rc::new(Cell::new(false));
2100 cx_a.update({
2101 let notified = a_notified.clone();
2102 |cx| {
2103 cx.observe(&active_call_a, move |_, _| notified.set(true))
2104 .detach()
2105 }
2106 });
2107
2108 let b_notified = Rc::new(Cell::new(false));
2109 cx_b.update({
2110 let b_notified = b_notified.clone();
2111 |cx| {
2112 cx.observe(&active_call_b, move |_, _| b_notified.set(true))
2113 .detach()
2114 }
2115 });
2116
2117 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
2118 active_call_a
2119 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2120 .await
2121 .unwrap();
2122 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
2123
2124 server
2125 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2126 .await;
2127
2128 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
2129
2130 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
2131 executor.run_until_parked();
2132 assert!(a_notified.take());
2133 assert_eq!(
2134 participant_locations(&room_a, cx_a),
2135 vec![("user_b".to_string(), ParticipantLocation::External)]
2136 );
2137 assert!(b_notified.take());
2138 assert_eq!(
2139 participant_locations(&room_b, cx_b),
2140 vec![("user_a".to_string(), ParticipantLocation::UnsharedProject)]
2141 );
2142
2143 let project_a_id = active_call_a
2144 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2145 .await
2146 .unwrap();
2147 executor.run_until_parked();
2148 assert!(a_notified.take());
2149 assert_eq!(
2150 participant_locations(&room_a, cx_a),
2151 vec![("user_b".to_string(), ParticipantLocation::External)]
2152 );
2153 assert!(b_notified.take());
2154 assert_eq!(
2155 participant_locations(&room_b, cx_b),
2156 vec![(
2157 "user_a".to_string(),
2158 ParticipantLocation::SharedProject {
2159 project_id: project_a_id
2160 }
2161 )]
2162 );
2163
2164 let project_b_id = active_call_b
2165 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
2166 .await
2167 .unwrap();
2168 executor.run_until_parked();
2169 assert!(a_notified.take());
2170 assert_eq!(
2171 participant_locations(&room_a, cx_a),
2172 vec![("user_b".to_string(), ParticipantLocation::External)]
2173 );
2174 assert!(b_notified.take());
2175 assert_eq!(
2176 participant_locations(&room_b, cx_b),
2177 vec![(
2178 "user_a".to_string(),
2179 ParticipantLocation::SharedProject {
2180 project_id: project_a_id
2181 }
2182 )]
2183 );
2184
2185 active_call_b
2186 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2187 .await
2188 .unwrap();
2189 executor.run_until_parked();
2190 assert!(a_notified.take());
2191 assert_eq!(
2192 participant_locations(&room_a, cx_a),
2193 vec![(
2194 "user_b".to_string(),
2195 ParticipantLocation::SharedProject {
2196 project_id: project_b_id
2197 }
2198 )]
2199 );
2200 assert!(b_notified.take());
2201 assert_eq!(
2202 participant_locations(&room_b, cx_b),
2203 vec![(
2204 "user_a".to_string(),
2205 ParticipantLocation::SharedProject {
2206 project_id: project_a_id
2207 }
2208 )]
2209 );
2210
2211 active_call_b
2212 .update(cx_b, |call, cx| call.set_location(None, cx))
2213 .await
2214 .unwrap();
2215 executor.run_until_parked();
2216 assert!(a_notified.take());
2217 assert_eq!(
2218 participant_locations(&room_a, cx_a),
2219 vec![("user_b".to_string(), ParticipantLocation::External)]
2220 );
2221 assert!(b_notified.take());
2222 assert_eq!(
2223 participant_locations(&room_b, cx_b),
2224 vec![(
2225 "user_a".to_string(),
2226 ParticipantLocation::SharedProject {
2227 project_id: project_a_id
2228 }
2229 )]
2230 );
2231
2232 fn participant_locations(
2233 room: &Model<Room>,
2234 cx: &TestAppContext,
2235 ) -> Vec<(String, ParticipantLocation)> {
2236 room.read_with(cx, |room, _| {
2237 room.remote_participants()
2238 .values()
2239 .map(|participant| {
2240 (
2241 participant.user.github_login.to_string(),
2242 participant.location,
2243 )
2244 })
2245 .collect()
2246 })
2247 }
2248}
2249
2250#[gpui::test(iterations = 10)]
2251async fn test_propagate_saves_and_fs_changes(
2252 executor: BackgroundExecutor,
2253 cx_a: &mut TestAppContext,
2254 cx_b: &mut TestAppContext,
2255 cx_c: &mut TestAppContext,
2256) {
2257 let mut server = TestServer::start(executor.clone()).await;
2258 let client_a = server.create_client(cx_a, "user_a").await;
2259 let client_b = server.create_client(cx_b, "user_b").await;
2260 let client_c = server.create_client(cx_c, "user_c").await;
2261
2262 server
2263 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2264 .await;
2265 let active_call_a = cx_a.read(ActiveCall::global);
2266
2267 let rust = Arc::new(Language::new(
2268 LanguageConfig {
2269 name: "Rust".into(),
2270 matcher: LanguageMatcher {
2271 path_suffixes: vec!["rs".to_string()],
2272 ..Default::default()
2273 },
2274 ..Default::default()
2275 },
2276 Some(tree_sitter_rust::LANGUAGE.into()),
2277 ));
2278 let javascript = Arc::new(Language::new(
2279 LanguageConfig {
2280 name: "JavaScript".into(),
2281 matcher: LanguageMatcher {
2282 path_suffixes: vec!["js".to_string()],
2283 ..Default::default()
2284 },
2285 ..Default::default()
2286 },
2287 Some(tree_sitter_rust::LANGUAGE.into()),
2288 ));
2289 for client in [&client_a, &client_b, &client_c] {
2290 client.language_registry().add(rust.clone());
2291 client.language_registry().add(javascript.clone());
2292 }
2293
2294 client_a
2295 .fs()
2296 .insert_tree(
2297 "/a",
2298 json!({
2299 "file1.rs": "",
2300 "file2": ""
2301 }),
2302 )
2303 .await;
2304 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2305
2306 let worktree_a = project_a.read_with(cx_a, |p, cx| p.worktrees(cx).next().unwrap());
2307 let project_id = active_call_a
2308 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2309 .await
2310 .unwrap();
2311
2312 // Join that worktree as clients B and C.
2313 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2314 let project_c = client_c.join_remote_project(project_id, cx_c).await;
2315
2316 let worktree_b = project_b.read_with(cx_b, |p, cx| p.worktrees(cx).next().unwrap());
2317
2318 let worktree_c = project_c.read_with(cx_c, |p, cx| p.worktrees(cx).next().unwrap());
2319
2320 // Open and edit a buffer as both guests B and C.
2321 let buffer_b = project_b
2322 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2323 .await
2324 .unwrap();
2325 let buffer_c = project_c
2326 .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2327 .await
2328 .unwrap();
2329
2330 buffer_b.read_with(cx_b, |buffer, _| {
2331 assert_eq!(buffer.language().unwrap().name(), "Rust".into());
2332 });
2333
2334 buffer_c.read_with(cx_c, |buffer, _| {
2335 assert_eq!(buffer.language().unwrap().name(), "Rust".into());
2336 });
2337 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], None, cx));
2338 buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], None, cx));
2339
2340 // Open and edit that buffer as the host.
2341 let buffer_a = project_a
2342 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2343 .await
2344 .unwrap();
2345
2346 executor.run_until_parked();
2347
2348 buffer_a.read_with(cx_a, |buf, _| assert_eq!(buf.text(), "i-am-c, i-am-b, "));
2349 buffer_a.update(cx_a, |buf, cx| {
2350 buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx)
2351 });
2352
2353 executor.run_until_parked();
2354
2355 buffer_a.read_with(cx_a, |buf, _| {
2356 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2357 });
2358
2359 buffer_b.read_with(cx_b, |buf, _| {
2360 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2361 });
2362
2363 buffer_c.read_with(cx_c, |buf, _| {
2364 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2365 });
2366
2367 // Edit the buffer as the host and concurrently save as guest B.
2368 let save_b = project_b.update(cx_b, |project, cx| {
2369 project.save_buffer(buffer_b.clone(), cx)
2370 });
2371 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
2372 save_b.await.unwrap();
2373 assert_eq!(
2374 client_a.fs().load("/a/file1.rs".as_ref()).await.unwrap(),
2375 "hi-a, i-am-c, i-am-b, i-am-a"
2376 );
2377
2378 executor.run_until_parked();
2379
2380 buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty()));
2381
2382 buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty()));
2383
2384 buffer_c.read_with(cx_c, |buf, _| assert!(!buf.is_dirty()));
2385
2386 // Make changes on host's file system, see those changes on guest worktrees.
2387 client_a
2388 .fs()
2389 .rename(
2390 "/a/file1.rs".as_ref(),
2391 "/a/file1.js".as_ref(),
2392 Default::default(),
2393 )
2394 .await
2395 .unwrap();
2396 client_a
2397 .fs()
2398 .rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
2399 .await
2400 .unwrap();
2401 client_a.fs().insert_file("/a/file4", "4".into()).await;
2402 executor.run_until_parked();
2403
2404 worktree_a.read_with(cx_a, |tree, _| {
2405 assert_eq!(
2406 tree.paths()
2407 .map(|p| p.to_string_lossy())
2408 .collect::<Vec<_>>(),
2409 ["file1.js", "file3", "file4"]
2410 )
2411 });
2412
2413 worktree_b.read_with(cx_b, |tree, _| {
2414 assert_eq!(
2415 tree.paths()
2416 .map(|p| p.to_string_lossy())
2417 .collect::<Vec<_>>(),
2418 ["file1.js", "file3", "file4"]
2419 )
2420 });
2421
2422 worktree_c.read_with(cx_c, |tree, _| {
2423 assert_eq!(
2424 tree.paths()
2425 .map(|p| p.to_string_lossy())
2426 .collect::<Vec<_>>(),
2427 ["file1.js", "file3", "file4"]
2428 )
2429 });
2430
2431 // Ensure buffer files are updated as well.
2432
2433 buffer_a.read_with(cx_a, |buffer, _| {
2434 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2435 assert_eq!(buffer.language().unwrap().name(), "JavaScript".into());
2436 });
2437
2438 buffer_b.read_with(cx_b, |buffer, _| {
2439 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2440 assert_eq!(buffer.language().unwrap().name(), "JavaScript".into());
2441 });
2442
2443 buffer_c.read_with(cx_c, |buffer, _| {
2444 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2445 assert_eq!(buffer.language().unwrap().name(), "JavaScript".into());
2446 });
2447
2448 let new_buffer_a = project_a
2449 .update(cx_a, |p, cx| p.create_buffer(cx))
2450 .await
2451 .unwrap();
2452
2453 let new_buffer_id = new_buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id());
2454 let new_buffer_b = project_b
2455 .update(cx_b, |p, cx| p.open_buffer_by_id(new_buffer_id, cx))
2456 .await
2457 .unwrap();
2458
2459 new_buffer_b.read_with(cx_b, |buffer, _| {
2460 assert!(buffer.file().is_none());
2461 });
2462
2463 new_buffer_a.update(cx_a, |buffer, cx| {
2464 buffer.edit([(0..0, "ok")], None, cx);
2465 });
2466 project_a
2467 .update(cx_a, |project, cx| {
2468 let path = ProjectPath {
2469 path: Arc::from(Path::new("file3.rs")),
2470 worktree_id: worktree_a.read(cx).id(),
2471 };
2472
2473 project.save_buffer_as(new_buffer_a.clone(), path, cx)
2474 })
2475 .await
2476 .unwrap();
2477
2478 executor.run_until_parked();
2479
2480 new_buffer_b.read_with(cx_b, |buffer_b, _| {
2481 assert_eq!(
2482 buffer_b.file().unwrap().path().as_ref(),
2483 Path::new("file3.rs")
2484 );
2485
2486 new_buffer_a.read_with(cx_a, |buffer_a, _| {
2487 assert_eq!(buffer_b.saved_mtime(), buffer_a.saved_mtime());
2488 assert_eq!(buffer_b.saved_version(), buffer_a.saved_version());
2489 });
2490 });
2491}
2492
2493#[gpui::test(iterations = 10)]
2494async fn test_git_diff_base_change(
2495 executor: BackgroundExecutor,
2496 cx_a: &mut TestAppContext,
2497 cx_b: &mut TestAppContext,
2498) {
2499 let mut server = TestServer::start(executor.clone()).await;
2500 let client_a = server.create_client(cx_a, "user_a").await;
2501 let client_b = server.create_client(cx_b, "user_b").await;
2502 server
2503 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2504 .await;
2505 let active_call_a = cx_a.read(ActiveCall::global);
2506
2507 client_a
2508 .fs()
2509 .insert_tree(
2510 "/dir",
2511 json!({
2512 ".git": {},
2513 "sub": {
2514 ".git": {},
2515 "b.txt": "
2516 one
2517 two
2518 three
2519 ".unindent(),
2520 },
2521 "a.txt": "
2522 one
2523 two
2524 three
2525 ".unindent(),
2526 }),
2527 )
2528 .await;
2529
2530 let (project_local, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2531 let project_id = active_call_a
2532 .update(cx_a, |call, cx| {
2533 call.share_project(project_local.clone(), cx)
2534 })
2535 .await
2536 .unwrap();
2537
2538 let project_remote = client_b.join_remote_project(project_id, cx_b).await;
2539
2540 let diff_base = "
2541 one
2542 three
2543 "
2544 .unindent();
2545
2546 let new_diff_base = "
2547 one
2548 two
2549 "
2550 .unindent();
2551
2552 client_a.fs().set_index_for_repo(
2553 Path::new("/dir/.git"),
2554 &[(Path::new("a.txt"), diff_base.clone())],
2555 );
2556
2557 // Create the buffer
2558 let buffer_local_a = project_local
2559 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2560 .await
2561 .unwrap();
2562
2563 // Wait for it to catch up to the new diff
2564 executor.run_until_parked();
2565
2566 // Smoke test diffing
2567
2568 buffer_local_a.read_with(cx_a, |buffer, _| {
2569 assert_eq!(
2570 buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
2571 Some(diff_base.as_str())
2572 );
2573 git::diff::assert_hunks(
2574 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2575 buffer,
2576 &diff_base,
2577 &[(1..2, "", "two\n")],
2578 );
2579 });
2580
2581 // Create remote buffer
2582 let buffer_remote_a = project_remote
2583 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2584 .await
2585 .unwrap();
2586
2587 // Wait remote buffer to catch up to the new diff
2588 executor.run_until_parked();
2589
2590 // Smoke test diffing
2591
2592 buffer_remote_a.read_with(cx_b, |buffer, _| {
2593 assert_eq!(
2594 buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
2595 Some(diff_base.as_str())
2596 );
2597 git::diff::assert_hunks(
2598 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2599 buffer,
2600 &diff_base,
2601 &[(1..2, "", "two\n")],
2602 );
2603 });
2604
2605 client_a.fs().set_index_for_repo(
2606 Path::new("/dir/.git"),
2607 &[(Path::new("a.txt"), new_diff_base.clone())],
2608 );
2609
2610 // Wait for buffer_local_a to receive it
2611 executor.run_until_parked();
2612
2613 // Smoke test new diffing
2614
2615 buffer_local_a.read_with(cx_a, |buffer, _| {
2616 assert_eq!(
2617 buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
2618 Some(new_diff_base.as_str())
2619 );
2620
2621 git::diff::assert_hunks(
2622 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2623 buffer,
2624 &diff_base,
2625 &[(2..3, "", "three\n")],
2626 );
2627 });
2628
2629 // Smoke test B
2630
2631 buffer_remote_a.read_with(cx_b, |buffer, _| {
2632 assert_eq!(
2633 buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
2634 Some(new_diff_base.as_str())
2635 );
2636 git::diff::assert_hunks(
2637 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2638 buffer,
2639 &diff_base,
2640 &[(2..3, "", "three\n")],
2641 );
2642 });
2643
2644 //Nested git dir
2645
2646 let diff_base = "
2647 one
2648 three
2649 "
2650 .unindent();
2651
2652 let new_diff_base = "
2653 one
2654 two
2655 "
2656 .unindent();
2657
2658 client_a.fs().set_index_for_repo(
2659 Path::new("/dir/sub/.git"),
2660 &[(Path::new("b.txt"), diff_base.clone())],
2661 );
2662
2663 // Create the buffer
2664 let buffer_local_b = project_local
2665 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2666 .await
2667 .unwrap();
2668
2669 // Wait for it to catch up to the new diff
2670 executor.run_until_parked();
2671
2672 // Smoke test diffing
2673
2674 buffer_local_b.read_with(cx_a, |buffer, _| {
2675 assert_eq!(
2676 buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
2677 Some(diff_base.as_str())
2678 );
2679 git::diff::assert_hunks(
2680 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2681 buffer,
2682 &diff_base,
2683 &[(1..2, "", "two\n")],
2684 );
2685 });
2686
2687 // Create remote buffer
2688 let buffer_remote_b = project_remote
2689 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2690 .await
2691 .unwrap();
2692
2693 // Wait remote buffer to catch up to the new diff
2694 executor.run_until_parked();
2695
2696 // Smoke test diffing
2697
2698 buffer_remote_b.read_with(cx_b, |buffer, _| {
2699 assert_eq!(
2700 buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
2701 Some(diff_base.as_str())
2702 );
2703 git::diff::assert_hunks(
2704 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2705 buffer,
2706 &diff_base,
2707 &[(1..2, "", "two\n")],
2708 );
2709 });
2710
2711 client_a.fs().set_index_for_repo(
2712 Path::new("/dir/sub/.git"),
2713 &[(Path::new("b.txt"), new_diff_base.clone())],
2714 );
2715
2716 // Wait for buffer_local_b to receive it
2717 executor.run_until_parked();
2718
2719 // Smoke test new diffing
2720
2721 buffer_local_b.read_with(cx_a, |buffer, _| {
2722 assert_eq!(
2723 buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
2724 Some(new_diff_base.as_str())
2725 );
2726 println!("{:?}", buffer.as_rope().to_string());
2727 println!("{:?}", buffer.diff_base());
2728 println!(
2729 "{:?}",
2730 buffer
2731 .snapshot()
2732 .git_diff_hunks_in_row_range(0..4)
2733 .collect::<Vec<_>>()
2734 );
2735
2736 git::diff::assert_hunks(
2737 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2738 buffer,
2739 &diff_base,
2740 &[(2..3, "", "three\n")],
2741 );
2742 });
2743
2744 // Smoke test B
2745
2746 buffer_remote_b.read_with(cx_b, |buffer, _| {
2747 assert_eq!(
2748 buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
2749 Some(new_diff_base.as_str())
2750 );
2751 git::diff::assert_hunks(
2752 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2753 buffer,
2754 &diff_base,
2755 &[(2..3, "", "three\n")],
2756 );
2757 });
2758}
2759
2760#[gpui::test]
2761async fn test_git_branch_name(
2762 executor: BackgroundExecutor,
2763 cx_a: &mut TestAppContext,
2764 cx_b: &mut TestAppContext,
2765 cx_c: &mut TestAppContext,
2766) {
2767 let mut server = TestServer::start(executor.clone()).await;
2768 let client_a = server.create_client(cx_a, "user_a").await;
2769 let client_b = server.create_client(cx_b, "user_b").await;
2770 let client_c = server.create_client(cx_c, "user_c").await;
2771 server
2772 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2773 .await;
2774 let active_call_a = cx_a.read(ActiveCall::global);
2775
2776 client_a
2777 .fs()
2778 .insert_tree(
2779 "/dir",
2780 json!({
2781 ".git": {},
2782 }),
2783 )
2784 .await;
2785
2786 let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2787 let project_id = active_call_a
2788 .update(cx_a, |call, cx| {
2789 call.share_project(project_local.clone(), cx)
2790 })
2791 .await
2792 .unwrap();
2793
2794 let project_remote = client_b.join_remote_project(project_id, cx_b).await;
2795 client_a
2796 .fs()
2797 .set_branch_name(Path::new("/dir/.git"), Some("branch-1"));
2798
2799 // Wait for it to catch up to the new branch
2800 executor.run_until_parked();
2801
2802 #[track_caller]
2803 fn assert_branch(branch_name: Option<impl Into<String>>, project: &Project, cx: &AppContext) {
2804 let branch_name = branch_name.map(Into::into);
2805 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
2806 assert_eq!(worktrees.len(), 1);
2807 let worktree = worktrees[0].clone();
2808 let root_entry = worktree.read(cx).snapshot().root_git_entry().unwrap();
2809 assert_eq!(root_entry.branch(), branch_name.map(Into::into));
2810 }
2811
2812 // Smoke test branch reading
2813
2814 project_local.read_with(cx_a, |project, cx| {
2815 assert_branch(Some("branch-1"), project, cx)
2816 });
2817
2818 project_remote.read_with(cx_b, |project, cx| {
2819 assert_branch(Some("branch-1"), project, cx)
2820 });
2821
2822 client_a
2823 .fs()
2824 .set_branch_name(Path::new("/dir/.git"), Some("branch-2"));
2825
2826 // Wait for buffer_local_a to receive it
2827 executor.run_until_parked();
2828
2829 // Smoke test branch reading
2830
2831 project_local.read_with(cx_a, |project, cx| {
2832 assert_branch(Some("branch-2"), project, cx)
2833 });
2834
2835 project_remote.read_with(cx_b, |project, cx| {
2836 assert_branch(Some("branch-2"), project, cx)
2837 });
2838
2839 let project_remote_c = client_c.join_remote_project(project_id, cx_c).await;
2840 executor.run_until_parked();
2841
2842 project_remote_c.read_with(cx_c, |project, cx| {
2843 assert_branch(Some("branch-2"), project, cx)
2844 });
2845}
2846
2847#[gpui::test]
2848async fn test_git_status_sync(
2849 executor: BackgroundExecutor,
2850 cx_a: &mut TestAppContext,
2851 cx_b: &mut TestAppContext,
2852 cx_c: &mut TestAppContext,
2853) {
2854 let mut server = TestServer::start(executor.clone()).await;
2855 let client_a = server.create_client(cx_a, "user_a").await;
2856 let client_b = server.create_client(cx_b, "user_b").await;
2857 let client_c = server.create_client(cx_c, "user_c").await;
2858 server
2859 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2860 .await;
2861 let active_call_a = cx_a.read(ActiveCall::global);
2862
2863 client_a
2864 .fs()
2865 .insert_tree(
2866 "/dir",
2867 json!({
2868 ".git": {},
2869 "a.txt": "a",
2870 "b.txt": "b",
2871 }),
2872 )
2873 .await;
2874
2875 const A_TXT: &str = "a.txt";
2876 const B_TXT: &str = "b.txt";
2877
2878 client_a.fs().set_status_for_repo_via_git_operation(
2879 Path::new("/dir/.git"),
2880 &[
2881 (Path::new(A_TXT), GitFileStatus::Added),
2882 (Path::new(B_TXT), GitFileStatus::Added),
2883 ],
2884 );
2885
2886 let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2887 let project_id = active_call_a
2888 .update(cx_a, |call, cx| {
2889 call.share_project(project_local.clone(), cx)
2890 })
2891 .await
2892 .unwrap();
2893
2894 let project_remote = client_b.join_remote_project(project_id, cx_b).await;
2895
2896 // Wait for it to catch up to the new status
2897 executor.run_until_parked();
2898
2899 #[track_caller]
2900 fn assert_status(
2901 file: &impl AsRef<Path>,
2902 status: Option<GitFileStatus>,
2903 project: &Project,
2904 cx: &AppContext,
2905 ) {
2906 let file = file.as_ref();
2907 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
2908 assert_eq!(worktrees.len(), 1);
2909 let worktree = worktrees[0].clone();
2910 let snapshot = worktree.read(cx).snapshot();
2911 assert_eq!(snapshot.status_for_file(file), status);
2912 }
2913
2914 // Smoke test status reading
2915
2916 project_local.read_with(cx_a, |project, cx| {
2917 assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
2918 assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
2919 });
2920
2921 project_remote.read_with(cx_b, |project, cx| {
2922 assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
2923 assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
2924 });
2925
2926 client_a.fs().set_status_for_repo_via_working_copy_change(
2927 Path::new("/dir/.git"),
2928 &[
2929 (Path::new(A_TXT), GitFileStatus::Modified),
2930 (Path::new(B_TXT), GitFileStatus::Modified),
2931 ],
2932 );
2933
2934 // Wait for buffer_local_a to receive it
2935 executor.run_until_parked();
2936
2937 // Smoke test status reading
2938
2939 project_local.read_with(cx_a, |project, cx| {
2940 assert_status(
2941 &Path::new(A_TXT),
2942 Some(GitFileStatus::Modified),
2943 project,
2944 cx,
2945 );
2946 assert_status(
2947 &Path::new(B_TXT),
2948 Some(GitFileStatus::Modified),
2949 project,
2950 cx,
2951 );
2952 });
2953
2954 project_remote.read_with(cx_b, |project, cx| {
2955 assert_status(
2956 &Path::new(A_TXT),
2957 Some(GitFileStatus::Modified),
2958 project,
2959 cx,
2960 );
2961 assert_status(
2962 &Path::new(B_TXT),
2963 Some(GitFileStatus::Modified),
2964 project,
2965 cx,
2966 );
2967 });
2968
2969 // And synchronization while joining
2970 let project_remote_c = client_c.join_remote_project(project_id, cx_c).await;
2971 executor.run_until_parked();
2972
2973 project_remote_c.read_with(cx_c, |project, cx| {
2974 assert_status(
2975 &Path::new(A_TXT),
2976 Some(GitFileStatus::Modified),
2977 project,
2978 cx,
2979 );
2980 assert_status(
2981 &Path::new(B_TXT),
2982 Some(GitFileStatus::Modified),
2983 project,
2984 cx,
2985 );
2986 });
2987}
2988
2989#[gpui::test(iterations = 10)]
2990async fn test_fs_operations(
2991 executor: BackgroundExecutor,
2992 cx_a: &mut TestAppContext,
2993 cx_b: &mut TestAppContext,
2994) {
2995 let mut server = TestServer::start(executor.clone()).await;
2996 let client_a = server.create_client(cx_a, "user_a").await;
2997 let client_b = server.create_client(cx_b, "user_b").await;
2998 server
2999 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3000 .await;
3001 let active_call_a = cx_a.read(ActiveCall::global);
3002
3003 client_a
3004 .fs()
3005 .insert_tree(
3006 "/dir",
3007 json!({
3008 "a.txt": "a-contents",
3009 "b.txt": "b-contents",
3010 }),
3011 )
3012 .await;
3013 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3014 let project_id = active_call_a
3015 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3016 .await
3017 .unwrap();
3018 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3019
3020 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
3021 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
3022
3023 let entry = project_b
3024 .update(cx_b, |project, cx| {
3025 project.create_entry((worktree_id, "c.txt"), false, cx)
3026 })
3027 .await
3028 .unwrap()
3029 .to_included()
3030 .unwrap();
3031
3032 worktree_a.read_with(cx_a, |worktree, _| {
3033 assert_eq!(
3034 worktree
3035 .paths()
3036 .map(|p| p.to_string_lossy())
3037 .collect::<Vec<_>>(),
3038 ["a.txt", "b.txt", "c.txt"]
3039 );
3040 });
3041
3042 worktree_b.read_with(cx_b, |worktree, _| {
3043 assert_eq!(
3044 worktree
3045 .paths()
3046 .map(|p| p.to_string_lossy())
3047 .collect::<Vec<_>>(),
3048 ["a.txt", "b.txt", "c.txt"]
3049 );
3050 });
3051
3052 project_b
3053 .update(cx_b, |project, cx| {
3054 project.rename_entry(entry.id, Path::new("d.txt"), cx)
3055 })
3056 .await
3057 .unwrap()
3058 .to_included()
3059 .unwrap();
3060
3061 worktree_a.read_with(cx_a, |worktree, _| {
3062 assert_eq!(
3063 worktree
3064 .paths()
3065 .map(|p| p.to_string_lossy())
3066 .collect::<Vec<_>>(),
3067 ["a.txt", "b.txt", "d.txt"]
3068 );
3069 });
3070
3071 worktree_b.read_with(cx_b, |worktree, _| {
3072 assert_eq!(
3073 worktree
3074 .paths()
3075 .map(|p| p.to_string_lossy())
3076 .collect::<Vec<_>>(),
3077 ["a.txt", "b.txt", "d.txt"]
3078 );
3079 });
3080
3081 let dir_entry = project_b
3082 .update(cx_b, |project, cx| {
3083 project.create_entry((worktree_id, "DIR"), true, cx)
3084 })
3085 .await
3086 .unwrap()
3087 .to_included()
3088 .unwrap();
3089
3090 worktree_a.read_with(cx_a, |worktree, _| {
3091 assert_eq!(
3092 worktree
3093 .paths()
3094 .map(|p| p.to_string_lossy())
3095 .collect::<Vec<_>>(),
3096 ["DIR", "a.txt", "b.txt", "d.txt"]
3097 );
3098 });
3099
3100 worktree_b.read_with(cx_b, |worktree, _| {
3101 assert_eq!(
3102 worktree
3103 .paths()
3104 .map(|p| p.to_string_lossy())
3105 .collect::<Vec<_>>(),
3106 ["DIR", "a.txt", "b.txt", "d.txt"]
3107 );
3108 });
3109
3110 project_b
3111 .update(cx_b, |project, cx| {
3112 project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
3113 })
3114 .await
3115 .unwrap()
3116 .to_included()
3117 .unwrap();
3118
3119 project_b
3120 .update(cx_b, |project, cx| {
3121 project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
3122 })
3123 .await
3124 .unwrap()
3125 .to_included()
3126 .unwrap();
3127
3128 project_b
3129 .update(cx_b, |project, cx| {
3130 project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
3131 })
3132 .await
3133 .unwrap()
3134 .to_included()
3135 .unwrap();
3136
3137 worktree_a.read_with(cx_a, |worktree, _| {
3138 assert_eq!(
3139 worktree
3140 .paths()
3141 .map(|p| p.to_string_lossy())
3142 .collect::<Vec<_>>(),
3143 [
3144 "DIR",
3145 "DIR/SUBDIR",
3146 "DIR/SUBDIR/f.txt",
3147 "DIR/e.txt",
3148 "a.txt",
3149 "b.txt",
3150 "d.txt"
3151 ]
3152 );
3153 });
3154
3155 worktree_b.read_with(cx_b, |worktree, _| {
3156 assert_eq!(
3157 worktree
3158 .paths()
3159 .map(|p| p.to_string_lossy())
3160 .collect::<Vec<_>>(),
3161 [
3162 "DIR",
3163 "DIR/SUBDIR",
3164 "DIR/SUBDIR/f.txt",
3165 "DIR/e.txt",
3166 "a.txt",
3167 "b.txt",
3168 "d.txt"
3169 ]
3170 );
3171 });
3172
3173 project_b
3174 .update(cx_b, |project, cx| {
3175 project.copy_entry(entry.id, None, Path::new("f.txt"), cx)
3176 })
3177 .await
3178 .unwrap()
3179 .unwrap();
3180
3181 worktree_a.read_with(cx_a, |worktree, _| {
3182 assert_eq!(
3183 worktree
3184 .paths()
3185 .map(|p| p.to_string_lossy())
3186 .collect::<Vec<_>>(),
3187 [
3188 "DIR",
3189 "DIR/SUBDIR",
3190 "DIR/SUBDIR/f.txt",
3191 "DIR/e.txt",
3192 "a.txt",
3193 "b.txt",
3194 "d.txt",
3195 "f.txt"
3196 ]
3197 );
3198 });
3199
3200 worktree_b.read_with(cx_b, |worktree, _| {
3201 assert_eq!(
3202 worktree
3203 .paths()
3204 .map(|p| p.to_string_lossy())
3205 .collect::<Vec<_>>(),
3206 [
3207 "DIR",
3208 "DIR/SUBDIR",
3209 "DIR/SUBDIR/f.txt",
3210 "DIR/e.txt",
3211 "a.txt",
3212 "b.txt",
3213 "d.txt",
3214 "f.txt"
3215 ]
3216 );
3217 });
3218
3219 project_b
3220 .update(cx_b, |project, cx| {
3221 project.delete_entry(dir_entry.id, false, cx).unwrap()
3222 })
3223 .await
3224 .unwrap();
3225 executor.run_until_parked();
3226
3227 worktree_a.read_with(cx_a, |worktree, _| {
3228 assert_eq!(
3229 worktree
3230 .paths()
3231 .map(|p| p.to_string_lossy())
3232 .collect::<Vec<_>>(),
3233 ["a.txt", "b.txt", "d.txt", "f.txt"]
3234 );
3235 });
3236
3237 worktree_b.read_with(cx_b, |worktree, _| {
3238 assert_eq!(
3239 worktree
3240 .paths()
3241 .map(|p| p.to_string_lossy())
3242 .collect::<Vec<_>>(),
3243 ["a.txt", "b.txt", "d.txt", "f.txt"]
3244 );
3245 });
3246
3247 project_b
3248 .update(cx_b, |project, cx| {
3249 project.delete_entry(entry.id, false, cx).unwrap()
3250 })
3251 .await
3252 .unwrap();
3253
3254 worktree_a.read_with(cx_a, |worktree, _| {
3255 assert_eq!(
3256 worktree
3257 .paths()
3258 .map(|p| p.to_string_lossy())
3259 .collect::<Vec<_>>(),
3260 ["a.txt", "b.txt", "f.txt"]
3261 );
3262 });
3263
3264 worktree_b.read_with(cx_b, |worktree, _| {
3265 assert_eq!(
3266 worktree
3267 .paths()
3268 .map(|p| p.to_string_lossy())
3269 .collect::<Vec<_>>(),
3270 ["a.txt", "b.txt", "f.txt"]
3271 );
3272 });
3273}
3274
3275#[gpui::test(iterations = 10)]
3276async fn test_local_settings(
3277 executor: BackgroundExecutor,
3278 cx_a: &mut TestAppContext,
3279 cx_b: &mut TestAppContext,
3280) {
3281 let mut server = TestServer::start(executor.clone()).await;
3282 let client_a = server.create_client(cx_a, "user_a").await;
3283 let client_b = server.create_client(cx_b, "user_b").await;
3284 server
3285 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3286 .await;
3287 let active_call_a = cx_a.read(ActiveCall::global);
3288
3289 // As client A, open a project that contains some local settings files
3290 client_a
3291 .fs()
3292 .insert_tree(
3293 "/dir",
3294 json!({
3295 ".zed": {
3296 "settings.json": r#"{ "tab_size": 2 }"#
3297 },
3298 "a": {
3299 ".zed": {
3300 "settings.json": r#"{ "tab_size": 8 }"#
3301 },
3302 "a.txt": "a-contents",
3303 },
3304 "b": {
3305 "b.txt": "b-contents",
3306 }
3307 }),
3308 )
3309 .await;
3310 let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
3311 executor.run_until_parked();
3312 let project_id = active_call_a
3313 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3314 .await
3315 .unwrap();
3316 executor.run_until_parked();
3317
3318 // As client B, join that project and observe the local settings.
3319 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3320
3321 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
3322 executor.run_until_parked();
3323 cx_b.read(|cx| {
3324 let store = cx.global::<SettingsStore>();
3325 assert_eq!(
3326 store
3327 .local_settings(worktree_b.read(cx).id())
3328 .collect::<Vec<_>>(),
3329 &[
3330 (Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
3331 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3332 ]
3333 )
3334 });
3335
3336 // As client A, update a settings file. As Client B, see the changed settings.
3337 client_a
3338 .fs()
3339 .insert_file("/dir/.zed/settings.json", r#"{}"#.into())
3340 .await;
3341 executor.run_until_parked();
3342 cx_b.read(|cx| {
3343 let store = cx.global::<SettingsStore>();
3344 assert_eq!(
3345 store
3346 .local_settings(worktree_b.read(cx).id())
3347 .collect::<Vec<_>>(),
3348 &[
3349 (Path::new("").into(), r#"{}"#.to_string()),
3350 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3351 ]
3352 )
3353 });
3354
3355 // As client A, create and remove some settings files. As client B, see the changed settings.
3356 client_a
3357 .fs()
3358 .remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
3359 .await
3360 .unwrap();
3361 client_a
3362 .fs()
3363 .create_dir("/dir/b/.zed".as_ref())
3364 .await
3365 .unwrap();
3366 client_a
3367 .fs()
3368 .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
3369 .await;
3370 executor.run_until_parked();
3371 cx_b.read(|cx| {
3372 let store = cx.global::<SettingsStore>();
3373 assert_eq!(
3374 store
3375 .local_settings(worktree_b.read(cx).id())
3376 .collect::<Vec<_>>(),
3377 &[
3378 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3379 (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
3380 ]
3381 )
3382 });
3383
3384 // As client B, disconnect.
3385 server.forbid_connections();
3386 server.disconnect_client(client_b.peer_id().unwrap());
3387
3388 // As client A, change and remove settings files while client B is disconnected.
3389 client_a
3390 .fs()
3391 .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
3392 .await;
3393 client_a
3394 .fs()
3395 .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
3396 .await
3397 .unwrap();
3398 executor.run_until_parked();
3399
3400 // As client B, reconnect and see the changed settings.
3401 server.allow_connections();
3402 executor.advance_clock(RECEIVE_TIMEOUT);
3403 cx_b.read(|cx| {
3404 let store = cx.global::<SettingsStore>();
3405 assert_eq!(
3406 store
3407 .local_settings(worktree_b.read(cx).id())
3408 .collect::<Vec<_>>(),
3409 &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
3410 )
3411 });
3412}
3413
3414#[gpui::test(iterations = 10)]
3415async fn test_buffer_conflict_after_save(
3416 executor: BackgroundExecutor,
3417 cx_a: &mut TestAppContext,
3418 cx_b: &mut TestAppContext,
3419) {
3420 let mut server = TestServer::start(executor.clone()).await;
3421 let client_a = server.create_client(cx_a, "user_a").await;
3422 let client_b = server.create_client(cx_b, "user_b").await;
3423 server
3424 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3425 .await;
3426 let active_call_a = cx_a.read(ActiveCall::global);
3427
3428 client_a
3429 .fs()
3430 .insert_tree(
3431 "/dir",
3432 json!({
3433 "a.txt": "a-contents",
3434 }),
3435 )
3436 .await;
3437 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3438 let project_id = active_call_a
3439 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3440 .await
3441 .unwrap();
3442 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3443
3444 // Open a buffer as client B
3445 let buffer_b = project_b
3446 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3447 .await
3448 .unwrap();
3449
3450 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
3451
3452 buffer_b.read_with(cx_b, |buf, _| {
3453 assert!(buf.is_dirty());
3454 assert!(!buf.has_conflict());
3455 });
3456
3457 project_b
3458 .update(cx_b, |project, cx| {
3459 project.save_buffer(buffer_b.clone(), cx)
3460 })
3461 .await
3462 .unwrap();
3463
3464 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
3465
3466 buffer_b.read_with(cx_b, |buf, _| {
3467 assert!(!buf.has_conflict());
3468 });
3469
3470 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
3471
3472 buffer_b.read_with(cx_b, |buf, _| {
3473 assert!(buf.is_dirty());
3474 assert!(!buf.has_conflict());
3475 });
3476}
3477
3478#[gpui::test(iterations = 10)]
3479async fn test_buffer_reloading(
3480 executor: BackgroundExecutor,
3481 cx_a: &mut TestAppContext,
3482 cx_b: &mut TestAppContext,
3483) {
3484 let mut server = TestServer::start(executor.clone()).await;
3485 let client_a = server.create_client(cx_a, "user_a").await;
3486 let client_b = server.create_client(cx_b, "user_b").await;
3487 server
3488 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3489 .await;
3490 let active_call_a = cx_a.read(ActiveCall::global);
3491
3492 client_a
3493 .fs()
3494 .insert_tree(
3495 "/dir",
3496 json!({
3497 "a.txt": "a\nb\nc",
3498 }),
3499 )
3500 .await;
3501 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3502 let project_id = active_call_a
3503 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3504 .await
3505 .unwrap();
3506 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3507
3508 // Open a buffer as client B
3509 let buffer_b = project_b
3510 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3511 .await
3512 .unwrap();
3513
3514 buffer_b.read_with(cx_b, |buf, _| {
3515 assert!(!buf.is_dirty());
3516 assert!(!buf.has_conflict());
3517 assert_eq!(buf.line_ending(), LineEnding::Unix);
3518 });
3519
3520 let new_contents = Rope::from("d\ne\nf");
3521 client_a
3522 .fs()
3523 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
3524 .await
3525 .unwrap();
3526
3527 executor.run_until_parked();
3528
3529 buffer_b.read_with(cx_b, |buf, _| {
3530 assert_eq!(buf.text(), new_contents.to_string());
3531 assert!(!buf.is_dirty());
3532 assert!(!buf.has_conflict());
3533 assert_eq!(buf.line_ending(), LineEnding::Windows);
3534 });
3535}
3536
3537#[gpui::test(iterations = 10)]
3538async fn test_editing_while_guest_opens_buffer(
3539 executor: BackgroundExecutor,
3540 cx_a: &mut TestAppContext,
3541 cx_b: &mut TestAppContext,
3542) {
3543 let mut server = TestServer::start(executor.clone()).await;
3544 let client_a = server.create_client(cx_a, "user_a").await;
3545 let client_b = server.create_client(cx_b, "user_b").await;
3546 server
3547 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3548 .await;
3549 let active_call_a = cx_a.read(ActiveCall::global);
3550
3551 client_a
3552 .fs()
3553 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3554 .await;
3555 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3556 let project_id = active_call_a
3557 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3558 .await
3559 .unwrap();
3560 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3561
3562 // Open a buffer as client A
3563 let buffer_a = project_a
3564 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3565 .await
3566 .unwrap();
3567
3568 // Start opening the same buffer as client B
3569 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3570 let buffer_b = cx_b.executor().spawn(open_buffer);
3571
3572 // Edit the buffer as client A while client B is still opening it.
3573 cx_b.executor().simulate_random_delay().await;
3574 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
3575 cx_b.executor().simulate_random_delay().await;
3576 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
3577
3578 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3579 let buffer_b = buffer_b.await.unwrap();
3580 executor.run_until_parked();
3581
3582 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3583}
3584
3585#[gpui::test(iterations = 10)]
3586async fn test_leaving_worktree_while_opening_buffer(
3587 executor: BackgroundExecutor,
3588 cx_a: &mut TestAppContext,
3589 cx_b: &mut TestAppContext,
3590) {
3591 let mut server = TestServer::start(executor.clone()).await;
3592 let client_a = server.create_client(cx_a, "user_a").await;
3593 let client_b = server.create_client(cx_b, "user_b").await;
3594 server
3595 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3596 .await;
3597 let active_call_a = cx_a.read(ActiveCall::global);
3598
3599 client_a
3600 .fs()
3601 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3602 .await;
3603 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3604 let project_id = active_call_a
3605 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3606 .await
3607 .unwrap();
3608 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3609
3610 // See that a guest has joined as client A.
3611 executor.run_until_parked();
3612
3613 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3614
3615 // Begin opening a buffer as client B, but leave the project before the open completes.
3616 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3617 let buffer_b = cx_b.executor().spawn(open_buffer);
3618 cx_b.update(|_| drop(project_b));
3619 drop(buffer_b);
3620
3621 // See that the guest has left.
3622 executor.run_until_parked();
3623
3624 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3625}
3626
3627#[gpui::test(iterations = 10)]
3628async fn test_canceling_buffer_opening(
3629 executor: BackgroundExecutor,
3630 cx_a: &mut TestAppContext,
3631 cx_b: &mut TestAppContext,
3632) {
3633 let mut server = TestServer::start(executor.clone()).await;
3634 let client_a = server.create_client(cx_a, "user_a").await;
3635 let client_b = server.create_client(cx_b, "user_b").await;
3636 server
3637 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3638 .await;
3639 let active_call_a = cx_a.read(ActiveCall::global);
3640
3641 client_a
3642 .fs()
3643 .insert_tree(
3644 "/dir",
3645 json!({
3646 "a.txt": "abc",
3647 }),
3648 )
3649 .await;
3650 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3651 let project_id = active_call_a
3652 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3653 .await
3654 .unwrap();
3655 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3656
3657 let buffer_a = project_a
3658 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3659 .await
3660 .unwrap();
3661
3662 // Open a buffer as client B but cancel after a random amount of time.
3663 let buffer_b = project_b.update(cx_b, |p, cx| {
3664 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3665 });
3666 executor.simulate_random_delay().await;
3667 drop(buffer_b);
3668
3669 // Try opening the same buffer again as client B, and ensure we can
3670 // still do it despite the cancellation above.
3671 let buffer_b = project_b
3672 .update(cx_b, |p, cx| {
3673 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3674 })
3675 .await
3676 .unwrap();
3677
3678 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3679}
3680
3681#[gpui::test(iterations = 10)]
3682async fn test_leaving_project(
3683 executor: BackgroundExecutor,
3684 cx_a: &mut TestAppContext,
3685 cx_b: &mut TestAppContext,
3686 cx_c: &mut TestAppContext,
3687) {
3688 let mut server = TestServer::start(executor.clone()).await;
3689 let client_a = server.create_client(cx_a, "user_a").await;
3690 let client_b = server.create_client(cx_b, "user_b").await;
3691 let client_c = server.create_client(cx_c, "user_c").await;
3692 server
3693 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3694 .await;
3695 let active_call_a = cx_a.read(ActiveCall::global);
3696
3697 client_a
3698 .fs()
3699 .insert_tree(
3700 "/a",
3701 json!({
3702 "a.txt": "a-contents",
3703 "b.txt": "b-contents",
3704 }),
3705 )
3706 .await;
3707 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3708 let project_id = active_call_a
3709 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3710 .await
3711 .unwrap();
3712 let project_b1 = client_b.join_remote_project(project_id, cx_b).await;
3713 let project_c = client_c.join_remote_project(project_id, cx_c).await;
3714
3715 // Client A sees that a guest has joined.
3716 executor.run_until_parked();
3717
3718 project_a.read_with(cx_a, |project, _| {
3719 assert_eq!(project.collaborators().len(), 2);
3720 });
3721
3722 project_b1.read_with(cx_b, |project, _| {
3723 assert_eq!(project.collaborators().len(), 2);
3724 });
3725
3726 project_c.read_with(cx_c, |project, _| {
3727 assert_eq!(project.collaborators().len(), 2);
3728 });
3729
3730 // Client B opens a buffer.
3731 let buffer_b1 = project_b1
3732 .update(cx_b, |project, cx| {
3733 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3734 project.open_buffer((worktree_id, "a.txt"), cx)
3735 })
3736 .await
3737 .unwrap();
3738
3739 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3740
3741 // Drop client B's project and ensure client A and client C observe client B leaving.
3742 cx_b.update(|_| drop(project_b1));
3743 executor.run_until_parked();
3744
3745 project_a.read_with(cx_a, |project, _| {
3746 assert_eq!(project.collaborators().len(), 1);
3747 });
3748
3749 project_c.read_with(cx_c, |project, _| {
3750 assert_eq!(project.collaborators().len(), 1);
3751 });
3752
3753 // Client B re-joins the project and can open buffers as before.
3754 let project_b2 = client_b.join_remote_project(project_id, cx_b).await;
3755 executor.run_until_parked();
3756
3757 project_a.read_with(cx_a, |project, _| {
3758 assert_eq!(project.collaborators().len(), 2);
3759 });
3760
3761 project_b2.read_with(cx_b, |project, _| {
3762 assert_eq!(project.collaborators().len(), 2);
3763 });
3764
3765 project_c.read_with(cx_c, |project, _| {
3766 assert_eq!(project.collaborators().len(), 2);
3767 });
3768
3769 let buffer_b2 = project_b2
3770 .update(cx_b, |project, cx| {
3771 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3772 project.open_buffer((worktree_id, "a.txt"), cx)
3773 })
3774 .await
3775 .unwrap();
3776
3777 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3778
3779 project_a.read_with(cx_a, |project, _| {
3780 assert_eq!(project.collaborators().len(), 2);
3781 });
3782
3783 // Drop client B's connection and ensure client A and client C observe client B leaving.
3784 client_b.disconnect(&cx_b.to_async());
3785 executor.advance_clock(RECONNECT_TIMEOUT);
3786
3787 project_a.read_with(cx_a, |project, _| {
3788 assert_eq!(project.collaborators().len(), 1);
3789 });
3790
3791 project_b2.read_with(cx_b, |project, cx| {
3792 assert!(project.is_disconnected(cx));
3793 });
3794
3795 project_c.read_with(cx_c, |project, _| {
3796 assert_eq!(project.collaborators().len(), 1);
3797 });
3798
3799 // Client B can't join the project, unless they re-join the room.
3800 cx_b.spawn(|cx| {
3801 Project::in_room(
3802 project_id,
3803 client_b.app_state.client.clone(),
3804 client_b.user_store().clone(),
3805 client_b.language_registry().clone(),
3806 FakeFs::new(cx.background_executor().clone()),
3807 cx,
3808 )
3809 })
3810 .await
3811 .unwrap_err();
3812
3813 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3814 client_c.wait_for_current_user(cx_c).await;
3815 server.forbid_connections();
3816 server.disconnect_client(client_c.peer_id().unwrap());
3817 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
3818 executor.run_until_parked();
3819
3820 project_a.read_with(cx_a, |project, _| {
3821 assert_eq!(project.collaborators().len(), 0);
3822 });
3823
3824 project_b2.read_with(cx_b, |project, cx| {
3825 assert!(project.is_disconnected(cx));
3826 });
3827
3828 project_c.read_with(cx_c, |project, cx| {
3829 assert!(project.is_disconnected(cx));
3830 });
3831}
3832
3833#[gpui::test(iterations = 10)]
3834async fn test_collaborating_with_diagnostics(
3835 executor: BackgroundExecutor,
3836 cx_a: &mut TestAppContext,
3837 cx_b: &mut TestAppContext,
3838 cx_c: &mut TestAppContext,
3839) {
3840 let mut server = TestServer::start(executor.clone()).await;
3841 let client_a = server.create_client(cx_a, "user_a").await;
3842 let client_b = server.create_client(cx_b, "user_b").await;
3843 let client_c = server.create_client(cx_c, "user_c").await;
3844 server
3845 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3846 .await;
3847 let active_call_a = cx_a.read(ActiveCall::global);
3848
3849 client_a.language_registry().add(Arc::new(Language::new(
3850 LanguageConfig {
3851 name: "Rust".into(),
3852 matcher: LanguageMatcher {
3853 path_suffixes: vec!["rs".to_string()],
3854 ..Default::default()
3855 },
3856 ..Default::default()
3857 },
3858 Some(tree_sitter_rust::LANGUAGE.into()),
3859 )));
3860 let mut fake_language_servers = client_a
3861 .language_registry()
3862 .register_fake_lsp("Rust", Default::default());
3863
3864 // Share a project as client A
3865 client_a
3866 .fs()
3867 .insert_tree(
3868 "/a",
3869 json!({
3870 "a.rs": "let one = two",
3871 "other.rs": "",
3872 }),
3873 )
3874 .await;
3875 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3876
3877 // Cause the language server to start.
3878 let _buffer = project_a
3879 .update(cx_a, |project, cx| {
3880 project.open_buffer(
3881 ProjectPath {
3882 worktree_id,
3883 path: Path::new("other.rs").into(),
3884 },
3885 cx,
3886 )
3887 })
3888 .await
3889 .unwrap();
3890
3891 // Simulate a language server reporting errors for a file.
3892 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3893 fake_language_server
3894 .receive_notification::<lsp::notification::DidOpenTextDocument>()
3895 .await;
3896 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3897 lsp::PublishDiagnosticsParams {
3898 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3899 version: None,
3900 diagnostics: vec![lsp::Diagnostic {
3901 severity: Some(lsp::DiagnosticSeverity::WARNING),
3902 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3903 message: "message 0".to_string(),
3904 ..Default::default()
3905 }],
3906 },
3907 );
3908
3909 // Client A shares the project and, simultaneously, the language server
3910 // publishes a diagnostic. This is done to ensure that the server always
3911 // observes the latest diagnostics for a worktree.
3912 let project_id = active_call_a
3913 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3914 .await
3915 .unwrap();
3916 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3917 lsp::PublishDiagnosticsParams {
3918 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3919 version: None,
3920 diagnostics: vec![lsp::Diagnostic {
3921 severity: Some(lsp::DiagnosticSeverity::ERROR),
3922 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3923 message: "message 1".to_string(),
3924 ..Default::default()
3925 }],
3926 },
3927 );
3928
3929 // Join the worktree as client B.
3930 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3931
3932 // Wait for server to see the diagnostics update.
3933 executor.run_until_parked();
3934
3935 // Ensure client B observes the new diagnostics.
3936
3937 project_b.read_with(cx_b, |project, cx| {
3938 assert_eq!(
3939 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3940 &[(
3941 ProjectPath {
3942 worktree_id,
3943 path: Arc::from(Path::new("a.rs")),
3944 },
3945 LanguageServerId(0),
3946 DiagnosticSummary {
3947 error_count: 1,
3948 warning_count: 0,
3949 },
3950 )]
3951 )
3952 });
3953
3954 // Join project as client C and observe the diagnostics.
3955 let project_c = client_c.join_remote_project(project_id, cx_c).await;
3956 executor.run_until_parked();
3957 let project_c_diagnostic_summaries =
3958 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
3959 project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
3960 })));
3961 project_c.update(cx_c, |_, cx| {
3962 let summaries = project_c_diagnostic_summaries.clone();
3963 cx.subscribe(&project_c, {
3964 move |p, _, event, cx| {
3965 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3966 *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
3967 }
3968 }
3969 })
3970 .detach();
3971 });
3972
3973 executor.run_until_parked();
3974 assert_eq!(
3975 project_c_diagnostic_summaries.borrow().as_slice(),
3976 &[(
3977 ProjectPath {
3978 worktree_id,
3979 path: Arc::from(Path::new("a.rs")),
3980 },
3981 LanguageServerId(0),
3982 DiagnosticSummary {
3983 error_count: 1,
3984 warning_count: 0,
3985 },
3986 )]
3987 );
3988
3989 // Simulate a language server reporting more errors for a file.
3990 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3991 lsp::PublishDiagnosticsParams {
3992 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3993 version: None,
3994 diagnostics: vec![
3995 lsp::Diagnostic {
3996 severity: Some(lsp::DiagnosticSeverity::ERROR),
3997 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3998 message: "message 1".to_string(),
3999 ..Default::default()
4000 },
4001 lsp::Diagnostic {
4002 severity: Some(lsp::DiagnosticSeverity::WARNING),
4003 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
4004 message: "message 2".to_string(),
4005 ..Default::default()
4006 },
4007 ],
4008 },
4009 );
4010
4011 // Clients B and C get the updated summaries
4012 executor.run_until_parked();
4013
4014 project_b.read_with(cx_b, |project, cx| {
4015 assert_eq!(
4016 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4017 [(
4018 ProjectPath {
4019 worktree_id,
4020 path: Arc::from(Path::new("a.rs")),
4021 },
4022 LanguageServerId(0),
4023 DiagnosticSummary {
4024 error_count: 1,
4025 warning_count: 1,
4026 },
4027 )]
4028 );
4029 });
4030
4031 project_c.read_with(cx_c, |project, cx| {
4032 assert_eq!(
4033 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4034 [(
4035 ProjectPath {
4036 worktree_id,
4037 path: Arc::from(Path::new("a.rs")),
4038 },
4039 LanguageServerId(0),
4040 DiagnosticSummary {
4041 error_count: 1,
4042 warning_count: 1,
4043 },
4044 )]
4045 );
4046 });
4047
4048 // Open the file with the errors on client B. They should be present.
4049 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4050 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4051
4052 buffer_b.read_with(cx_b, |buffer, _| {
4053 assert_eq!(
4054 buffer
4055 .snapshot()
4056 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
4057 .collect::<Vec<_>>(),
4058 &[
4059 DiagnosticEntry {
4060 range: Point::new(0, 4)..Point::new(0, 7),
4061 diagnostic: Diagnostic {
4062 group_id: 2,
4063 message: "message 1".to_string(),
4064 severity: lsp::DiagnosticSeverity::ERROR,
4065 is_primary: true,
4066 ..Default::default()
4067 }
4068 },
4069 DiagnosticEntry {
4070 range: Point::new(0, 10)..Point::new(0, 13),
4071 diagnostic: Diagnostic {
4072 group_id: 3,
4073 severity: lsp::DiagnosticSeverity::WARNING,
4074 message: "message 2".to_string(),
4075 is_primary: true,
4076 ..Default::default()
4077 }
4078 }
4079 ]
4080 );
4081 });
4082
4083 // Simulate a language server reporting no errors for a file.
4084 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4085 lsp::PublishDiagnosticsParams {
4086 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
4087 version: None,
4088 diagnostics: vec![],
4089 },
4090 );
4091 executor.run_until_parked();
4092
4093 project_a.read_with(cx_a, |project, cx| {
4094 assert_eq!(
4095 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4096 []
4097 )
4098 });
4099
4100 project_b.read_with(cx_b, |project, cx| {
4101 assert_eq!(
4102 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4103 []
4104 )
4105 });
4106
4107 project_c.read_with(cx_c, |project, cx| {
4108 assert_eq!(
4109 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4110 []
4111 )
4112 });
4113}
4114
4115#[gpui::test(iterations = 10)]
4116async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
4117 executor: BackgroundExecutor,
4118 cx_a: &mut TestAppContext,
4119 cx_b: &mut TestAppContext,
4120) {
4121 let mut server = TestServer::start(executor.clone()).await;
4122 let client_a = server.create_client(cx_a, "user_a").await;
4123 let client_b = server.create_client(cx_b, "user_b").await;
4124 server
4125 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4126 .await;
4127
4128 client_a.language_registry().add(rust_lang());
4129 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4130 "Rust",
4131 FakeLspAdapter {
4132 disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
4133 disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
4134 ..Default::default()
4135 },
4136 );
4137
4138 let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
4139 client_a
4140 .fs()
4141 .insert_tree(
4142 "/test",
4143 json!({
4144 "one.rs": "const ONE: usize = 1;",
4145 "two.rs": "const TWO: usize = 2;",
4146 "three.rs": "const THREE: usize = 3;",
4147 "four.rs": "const FOUR: usize = 3;",
4148 "five.rs": "const FIVE: usize = 3;",
4149 }),
4150 )
4151 .await;
4152
4153 let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
4154
4155 // Share a project as client A
4156 let active_call_a = cx_a.read(ActiveCall::global);
4157 let project_id = active_call_a
4158 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4159 .await
4160 .unwrap();
4161
4162 // Join the project as client B and open all three files.
4163 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4164 let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
4165 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
4166 }))
4167 .await
4168 .unwrap();
4169
4170 // Simulate a language server reporting errors for a file.
4171 let fake_language_server = fake_language_servers.next().await.unwrap();
4172 fake_language_server
4173 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
4174 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4175 })
4176 .await
4177 .unwrap();
4178 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4179 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4180 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
4181 lsp::WorkDoneProgressBegin {
4182 title: "Progress Began".into(),
4183 ..Default::default()
4184 },
4185 )),
4186 });
4187 for file_name in file_names {
4188 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4189 lsp::PublishDiagnosticsParams {
4190 uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
4191 version: None,
4192 diagnostics: vec![lsp::Diagnostic {
4193 severity: Some(lsp::DiagnosticSeverity::WARNING),
4194 source: Some("the-disk-based-diagnostics-source".into()),
4195 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
4196 message: "message one".to_string(),
4197 ..Default::default()
4198 }],
4199 },
4200 );
4201 }
4202 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4203 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4204 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
4205 lsp::WorkDoneProgressEnd { message: None },
4206 )),
4207 });
4208
4209 // When the "disk base diagnostics finished" message is received, the buffers'
4210 // diagnostics are expected to be present.
4211 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
4212 project_b.update(cx_b, {
4213 let project_b = project_b.clone();
4214 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
4215 move |_, cx| {
4216 cx.subscribe(&project_b, move |_, _, event, cx| {
4217 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
4218 disk_based_diagnostics_finished.store(true, SeqCst);
4219 for buffer in &guest_buffers {
4220 assert_eq!(
4221 buffer
4222 .read(cx)
4223 .snapshot()
4224 .diagnostics_in_range::<_, usize>(0..5, false)
4225 .count(),
4226 1,
4227 "expected a diagnostic for buffer {:?}",
4228 buffer.read(cx).file().unwrap().path(),
4229 );
4230 }
4231 }
4232 })
4233 .detach();
4234 }
4235 });
4236
4237 executor.run_until_parked();
4238 assert!(disk_based_diagnostics_finished.load(SeqCst));
4239}
4240
4241#[gpui::test(iterations = 10)]
4242async fn test_reloading_buffer_manually(
4243 executor: BackgroundExecutor,
4244 cx_a: &mut TestAppContext,
4245 cx_b: &mut TestAppContext,
4246) {
4247 let mut server = TestServer::start(executor.clone()).await;
4248 let client_a = server.create_client(cx_a, "user_a").await;
4249 let client_b = server.create_client(cx_b, "user_b").await;
4250 server
4251 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4252 .await;
4253 let active_call_a = cx_a.read(ActiveCall::global);
4254
4255 client_a
4256 .fs()
4257 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
4258 .await;
4259 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4260 let buffer_a = project_a
4261 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4262 .await
4263 .unwrap();
4264 let project_id = active_call_a
4265 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4266 .await
4267 .unwrap();
4268
4269 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4270
4271 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4272 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4273 buffer_b.update(cx_b, |buffer, cx| {
4274 buffer.edit([(4..7, "six")], None, cx);
4275 buffer.edit([(10..11, "6")], None, cx);
4276 assert_eq!(buffer.text(), "let six = 6;");
4277 assert!(buffer.is_dirty());
4278 assert!(!buffer.has_conflict());
4279 });
4280 executor.run_until_parked();
4281
4282 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4283
4284 client_a
4285 .fs()
4286 .save(
4287 "/a/a.rs".as_ref(),
4288 &Rope::from("let seven = 7;"),
4289 LineEnding::Unix,
4290 )
4291 .await
4292 .unwrap();
4293 executor.run_until_parked();
4294
4295 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4296
4297 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4298
4299 project_b
4300 .update(cx_b, |project, cx| {
4301 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4302 })
4303 .await
4304 .unwrap();
4305
4306 buffer_a.read_with(cx_a, |buffer, _| {
4307 assert_eq!(buffer.text(), "let seven = 7;");
4308 assert!(!buffer.is_dirty());
4309 assert!(!buffer.has_conflict());
4310 });
4311
4312 buffer_b.read_with(cx_b, |buffer, _| {
4313 assert_eq!(buffer.text(), "let seven = 7;");
4314 assert!(!buffer.is_dirty());
4315 assert!(!buffer.has_conflict());
4316 });
4317
4318 buffer_a.update(cx_a, |buffer, cx| {
4319 // Undoing on the host is a no-op when the reload was initiated by the guest.
4320 buffer.undo(cx);
4321 assert_eq!(buffer.text(), "let seven = 7;");
4322 assert!(!buffer.is_dirty());
4323 assert!(!buffer.has_conflict());
4324 });
4325 buffer_b.update(cx_b, |buffer, cx| {
4326 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4327 buffer.undo(cx);
4328 assert_eq!(buffer.text(), "let six = 6;");
4329 assert!(buffer.is_dirty());
4330 assert!(!buffer.has_conflict());
4331 });
4332}
4333
4334#[gpui::test(iterations = 10)]
4335async fn test_formatting_buffer(
4336 executor: BackgroundExecutor,
4337 cx_a: &mut TestAppContext,
4338 cx_b: &mut TestAppContext,
4339) {
4340 executor.allow_parking();
4341 let mut server = TestServer::start(executor.clone()).await;
4342 let client_a = server.create_client(cx_a, "user_a").await;
4343 let client_b = server.create_client(cx_b, "user_b").await;
4344 server
4345 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4346 .await;
4347 let active_call_a = cx_a.read(ActiveCall::global);
4348
4349 client_a.language_registry().add(rust_lang());
4350 let mut fake_language_servers = client_a
4351 .language_registry()
4352 .register_fake_lsp("Rust", FakeLspAdapter::default());
4353
4354 // Here we insert a fake tree with a directory that exists on disk. This is needed
4355 // because later we'll invoke a command, which requires passing a working directory
4356 // that points to a valid location on disk.
4357 let directory = env::current_dir().unwrap();
4358 client_a
4359 .fs()
4360 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4361 .await;
4362 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4363 let project_id = active_call_a
4364 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4365 .await
4366 .unwrap();
4367 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4368
4369 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4370 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4371
4372 let fake_language_server = fake_language_servers.next().await.unwrap();
4373 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4374 Ok(Some(vec![
4375 lsp::TextEdit {
4376 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4377 new_text: "h".to_string(),
4378 },
4379 lsp::TextEdit {
4380 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4381 new_text: "y".to_string(),
4382 },
4383 ]))
4384 });
4385
4386 project_b
4387 .update(cx_b, |project, cx| {
4388 project.format(
4389 HashSet::from_iter([buffer_b.clone()]),
4390 true,
4391 FormatTrigger::Save,
4392 FormatTarget::Buffer,
4393 cx,
4394 )
4395 })
4396 .await
4397 .unwrap();
4398
4399 // The edits from the LSP are applied, and a final newline is added.
4400 assert_eq!(
4401 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4402 "let honey = \"two\"\n"
4403 );
4404
4405 // Ensure buffer can be formatted using an external command. Notice how the
4406 // host's configuration is honored as opposed to using the guest's settings.
4407 cx_a.update(|cx| {
4408 SettingsStore::update_global(cx, |store, cx| {
4409 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4410 file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
4411 vec![Formatter::External {
4412 command: "awk".into(),
4413 arguments: Some(vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into()),
4414 }]
4415 .into(),
4416 )));
4417 });
4418 });
4419 });
4420 project_b
4421 .update(cx_b, |project, cx| {
4422 project.format(
4423 HashSet::from_iter([buffer_b.clone()]),
4424 true,
4425 FormatTrigger::Save,
4426 FormatTarget::Buffer,
4427 cx,
4428 )
4429 })
4430 .await
4431 .unwrap();
4432 assert_eq!(
4433 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4434 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4435 );
4436}
4437
4438#[gpui::test(iterations = 10)]
4439async fn test_prettier_formatting_buffer(
4440 executor: BackgroundExecutor,
4441 cx_a: &mut TestAppContext,
4442 cx_b: &mut TestAppContext,
4443) {
4444 let mut server = TestServer::start(executor.clone()).await;
4445 let client_a = server.create_client(cx_a, "user_a").await;
4446 let client_b = server.create_client(cx_b, "user_b").await;
4447 server
4448 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4449 .await;
4450 let active_call_a = cx_a.read(ActiveCall::global);
4451
4452 let test_plugin = "test_plugin";
4453
4454 client_a.language_registry().add(Arc::new(Language::new(
4455 LanguageConfig {
4456 name: "TypeScript".into(),
4457 matcher: LanguageMatcher {
4458 path_suffixes: vec!["ts".to_string()],
4459 ..Default::default()
4460 },
4461 ..Default::default()
4462 },
4463 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
4464 )));
4465 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4466 "TypeScript",
4467 FakeLspAdapter {
4468 prettier_plugins: vec![test_plugin],
4469 ..Default::default()
4470 },
4471 );
4472
4473 // Here we insert a fake tree with a directory that exists on disk. This is needed
4474 // because later we'll invoke a command, which requires passing a working directory
4475 // that points to a valid location on disk.
4476 let directory = env::current_dir().unwrap();
4477 let buffer_text = "let one = \"two\"";
4478 client_a
4479 .fs()
4480 .insert_tree(&directory, json!({ "a.ts": buffer_text }))
4481 .await;
4482 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4483 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
4484 let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
4485 let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
4486
4487 let project_id = active_call_a
4488 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4489 .await
4490 .unwrap();
4491 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4492 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
4493 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4494
4495 cx_a.update(|cx| {
4496 SettingsStore::update_global(cx, |store, cx| {
4497 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4498 file.defaults.formatter = Some(SelectedFormatter::Auto);
4499 file.defaults.prettier = Some(PrettierSettings {
4500 allowed: true,
4501 ..PrettierSettings::default()
4502 });
4503 });
4504 });
4505 });
4506 cx_b.update(|cx| {
4507 SettingsStore::update_global(cx, |store, cx| {
4508 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4509 file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
4510 vec![Formatter::LanguageServer { name: None }].into(),
4511 )));
4512 file.defaults.prettier = Some(PrettierSettings {
4513 allowed: true,
4514 ..PrettierSettings::default()
4515 });
4516 });
4517 });
4518 });
4519 let fake_language_server = fake_language_servers.next().await.unwrap();
4520 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4521 panic!(
4522 "Unexpected: prettier should be preferred since it's enabled and language supports it"
4523 )
4524 });
4525
4526 project_b
4527 .update(cx_b, |project, cx| {
4528 project.format(
4529 HashSet::from_iter([buffer_b.clone()]),
4530 true,
4531 FormatTrigger::Save,
4532 FormatTarget::Buffer,
4533 cx,
4534 )
4535 })
4536 .await
4537 .unwrap();
4538
4539 executor.run_until_parked();
4540 assert_eq!(
4541 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4542 buffer_text.to_string() + "\n" + prettier_format_suffix,
4543 "Prettier formatting was not applied to client buffer after client's request"
4544 );
4545
4546 project_a
4547 .update(cx_a, |project, cx| {
4548 project.format(
4549 HashSet::from_iter([buffer_a.clone()]),
4550 true,
4551 FormatTrigger::Manual,
4552 FormatTarget::Buffer,
4553 cx,
4554 )
4555 })
4556 .await
4557 .unwrap();
4558
4559 executor.run_until_parked();
4560 assert_eq!(
4561 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4562 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
4563 "Prettier formatting was not applied to client buffer after host's request"
4564 );
4565}
4566
4567#[gpui::test(iterations = 10)]
4568async fn test_definition(
4569 executor: BackgroundExecutor,
4570 cx_a: &mut TestAppContext,
4571 cx_b: &mut TestAppContext,
4572) {
4573 let mut server = TestServer::start(executor.clone()).await;
4574 let client_a = server.create_client(cx_a, "user_a").await;
4575 let client_b = server.create_client(cx_b, "user_b").await;
4576 server
4577 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4578 .await;
4579 let active_call_a = cx_a.read(ActiveCall::global);
4580
4581 let mut fake_language_servers = client_a
4582 .language_registry()
4583 .register_fake_lsp("Rust", Default::default());
4584 client_a.language_registry().add(rust_lang());
4585
4586 client_a
4587 .fs()
4588 .insert_tree(
4589 "/root",
4590 json!({
4591 "dir-1": {
4592 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4593 },
4594 "dir-2": {
4595 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4596 "c.rs": "type T2 = usize;",
4597 }
4598 }),
4599 )
4600 .await;
4601 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4602 let project_id = active_call_a
4603 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4604 .await
4605 .unwrap();
4606 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4607
4608 // Open the file on client B.
4609 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4610 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4611
4612 // Request the definition of a symbol as the guest.
4613 let fake_language_server = fake_language_servers.next().await.unwrap();
4614 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4615 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4616 lsp::Location::new(
4617 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4618 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4619 ),
4620 )))
4621 });
4622
4623 let definitions_1 = project_b
4624 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
4625 .await
4626 .unwrap();
4627 cx_b.read(|cx| {
4628 assert_eq!(definitions_1.len(), 1);
4629 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4630 let target_buffer = definitions_1[0].target.buffer.read(cx);
4631 assert_eq!(
4632 target_buffer.text(),
4633 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4634 );
4635 assert_eq!(
4636 definitions_1[0].target.range.to_point(target_buffer),
4637 Point::new(0, 6)..Point::new(0, 9)
4638 );
4639 });
4640
4641 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4642 // the previous call to `definition`.
4643 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4644 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4645 lsp::Location::new(
4646 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4647 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4648 ),
4649 )))
4650 });
4651
4652 let definitions_2 = project_b
4653 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4654 .await
4655 .unwrap();
4656 cx_b.read(|cx| {
4657 assert_eq!(definitions_2.len(), 1);
4658 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4659 let target_buffer = definitions_2[0].target.buffer.read(cx);
4660 assert_eq!(
4661 target_buffer.text(),
4662 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4663 );
4664 assert_eq!(
4665 definitions_2[0].target.range.to_point(target_buffer),
4666 Point::new(1, 6)..Point::new(1, 11)
4667 );
4668 });
4669 assert_eq!(
4670 definitions_1[0].target.buffer,
4671 definitions_2[0].target.buffer
4672 );
4673
4674 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4675 |req, _| async move {
4676 assert_eq!(
4677 req.text_document_position_params.position,
4678 lsp::Position::new(0, 7)
4679 );
4680 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4681 lsp::Location::new(
4682 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4683 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4684 ),
4685 )))
4686 },
4687 );
4688
4689 let type_definitions = project_b
4690 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4691 .await
4692 .unwrap();
4693 cx_b.read(|cx| {
4694 assert_eq!(type_definitions.len(), 1);
4695 let target_buffer = type_definitions[0].target.buffer.read(cx);
4696 assert_eq!(target_buffer.text(), "type T2 = usize;");
4697 assert_eq!(
4698 type_definitions[0].target.range.to_point(target_buffer),
4699 Point::new(0, 5)..Point::new(0, 7)
4700 );
4701 });
4702}
4703
4704#[gpui::test(iterations = 10)]
4705async fn test_references(
4706 executor: BackgroundExecutor,
4707 cx_a: &mut TestAppContext,
4708 cx_b: &mut TestAppContext,
4709) {
4710 let mut server = TestServer::start(executor.clone()).await;
4711 let client_a = server.create_client(cx_a, "user_a").await;
4712 let client_b = server.create_client(cx_b, "user_b").await;
4713 server
4714 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4715 .await;
4716 let active_call_a = cx_a.read(ActiveCall::global);
4717
4718 client_a.language_registry().add(rust_lang());
4719 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4720 "Rust",
4721 FakeLspAdapter {
4722 name: "my-fake-lsp-adapter",
4723 capabilities: lsp::ServerCapabilities {
4724 references_provider: Some(lsp::OneOf::Left(true)),
4725 ..Default::default()
4726 },
4727 ..Default::default()
4728 },
4729 );
4730
4731 client_a
4732 .fs()
4733 .insert_tree(
4734 "/root",
4735 json!({
4736 "dir-1": {
4737 "one.rs": "const ONE: usize = 1;",
4738 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4739 },
4740 "dir-2": {
4741 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4742 }
4743 }),
4744 )
4745 .await;
4746 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4747 let project_id = active_call_a
4748 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4749 .await
4750 .unwrap();
4751 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4752
4753 // Open the file on client B.
4754 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
4755 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4756
4757 // Request references to a symbol as the guest.
4758 let fake_language_server = fake_language_servers.next().await.unwrap();
4759 let (lsp_response_tx, rx) = mpsc::unbounded::<Result<Option<Vec<lsp::Location>>>>();
4760 fake_language_server.handle_request::<lsp::request::References, _, _>({
4761 let rx = Arc::new(Mutex::new(Some(rx)));
4762 move |params, _| {
4763 assert_eq!(
4764 params.text_document_position.text_document.uri.as_str(),
4765 "file:///root/dir-1/one.rs"
4766 );
4767 let rx = rx.clone();
4768 async move {
4769 let mut response_rx = rx.lock().take().unwrap();
4770 let result = response_rx.next().await.unwrap();
4771 *rx.lock() = Some(response_rx);
4772 result
4773 }
4774 }
4775 });
4776
4777 let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx));
4778
4779 // User is informed that a request is pending.
4780 executor.run_until_parked();
4781 project_b.read_with(cx_b, |project, cx| {
4782 let status = project.language_server_statuses(cx).next().unwrap().1;
4783 assert_eq!(status.name, "my-fake-lsp-adapter");
4784 assert_eq!(
4785 status.pending_work.values().next().unwrap().message,
4786 Some("Finding references...".into())
4787 );
4788 });
4789
4790 // Cause the language server to respond.
4791 lsp_response_tx
4792 .unbounded_send(Ok(Some(vec![
4793 lsp::Location {
4794 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4795 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4796 },
4797 lsp::Location {
4798 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4799 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4800 },
4801 lsp::Location {
4802 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4803 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4804 },
4805 ])))
4806 .unwrap();
4807
4808 let references = references.await.unwrap();
4809 executor.run_until_parked();
4810 project_b.read_with(cx_b, |project, cx| {
4811 // User is informed that a request is no longer pending.
4812 let status = project.language_server_statuses(cx).next().unwrap().1;
4813 assert!(status.pending_work.is_empty());
4814
4815 assert_eq!(references.len(), 3);
4816 assert_eq!(project.worktrees(cx).count(), 2);
4817
4818 let two_buffer = references[0].buffer.read(cx);
4819 let three_buffer = references[2].buffer.read(cx);
4820 assert_eq!(
4821 two_buffer.file().unwrap().path().as_ref(),
4822 Path::new("two.rs")
4823 );
4824 assert_eq!(references[1].buffer, references[0].buffer);
4825 assert_eq!(
4826 three_buffer.file().unwrap().full_path(cx),
4827 Path::new("/root/dir-2/three.rs")
4828 );
4829
4830 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4831 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4832 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4833 });
4834
4835 let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx));
4836
4837 // User is informed that a request is pending.
4838 executor.run_until_parked();
4839 project_b.read_with(cx_b, |project, cx| {
4840 let status = project.language_server_statuses(cx).next().unwrap().1;
4841 assert_eq!(status.name, "my-fake-lsp-adapter");
4842 assert_eq!(
4843 status.pending_work.values().next().unwrap().message,
4844 Some("Finding references...".into())
4845 );
4846 });
4847
4848 // Cause the LSP request to fail.
4849 lsp_response_tx
4850 .unbounded_send(Err(anyhow!("can't find references")))
4851 .unwrap();
4852 references.await.unwrap_err();
4853
4854 // User is informed that the request is no longer pending.
4855 executor.run_until_parked();
4856 project_b.read_with(cx_b, |project, cx| {
4857 let status = project.language_server_statuses(cx).next().unwrap().1;
4858 assert!(status.pending_work.is_empty());
4859 });
4860}
4861
4862#[gpui::test(iterations = 10)]
4863async fn test_project_search(
4864 executor: BackgroundExecutor,
4865 cx_a: &mut TestAppContext,
4866 cx_b: &mut TestAppContext,
4867) {
4868 let mut server = TestServer::start(executor.clone()).await;
4869 let client_a = server.create_client(cx_a, "user_a").await;
4870 let client_b = server.create_client(cx_b, "user_b").await;
4871 server
4872 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4873 .await;
4874 let active_call_a = cx_a.read(ActiveCall::global);
4875
4876 client_a
4877 .fs()
4878 .insert_tree(
4879 "/root",
4880 json!({
4881 "dir-1": {
4882 "a": "hello world",
4883 "b": "goodnight moon",
4884 "c": "a world of goo",
4885 "d": "world champion of clown world",
4886 },
4887 "dir-2": {
4888 "e": "disney world is fun",
4889 }
4890 }),
4891 )
4892 .await;
4893 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4894 let (worktree_2, _) = project_a
4895 .update(cx_a, |p, cx| {
4896 p.find_or_create_worktree("/root/dir-2", true, cx)
4897 })
4898 .await
4899 .unwrap();
4900 worktree_2
4901 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4902 .await;
4903 let project_id = active_call_a
4904 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4905 .await
4906 .unwrap();
4907
4908 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4909
4910 // Perform a search as the guest.
4911 let mut results = HashMap::default();
4912 let mut search_rx = project_b.update(cx_b, |project, cx| {
4913 project.search(
4914 SearchQuery::text(
4915 "world",
4916 false,
4917 false,
4918 false,
4919 Default::default(),
4920 Default::default(),
4921 None,
4922 )
4923 .unwrap(),
4924 cx,
4925 )
4926 });
4927 while let Some(result) = search_rx.next().await {
4928 match result {
4929 SearchResult::Buffer { buffer, ranges } => {
4930 results.entry(buffer).or_insert(ranges);
4931 }
4932 SearchResult::LimitReached => {
4933 panic!("Unexpectedly reached search limit in tests. If you do want to assert limit-reached, change this panic call.")
4934 }
4935 };
4936 }
4937
4938 let mut ranges_by_path = results
4939 .into_iter()
4940 .map(|(buffer, ranges)| {
4941 buffer.read_with(cx_b, |buffer, cx| {
4942 let path = buffer.file().unwrap().full_path(cx);
4943 let offset_ranges = ranges
4944 .into_iter()
4945 .map(|range| range.to_offset(buffer))
4946 .collect::<Vec<_>>();
4947 (path, offset_ranges)
4948 })
4949 })
4950 .collect::<Vec<_>>();
4951 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4952
4953 assert_eq!(
4954 ranges_by_path,
4955 &[
4956 (PathBuf::from("dir-1/a"), vec![6..11]),
4957 (PathBuf::from("dir-1/c"), vec![2..7]),
4958 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4959 (PathBuf::from("dir-2/e"), vec![7..12]),
4960 ]
4961 );
4962}
4963
4964#[gpui::test(iterations = 10)]
4965async fn test_document_highlights(
4966 executor: BackgroundExecutor,
4967 cx_a: &mut TestAppContext,
4968 cx_b: &mut TestAppContext,
4969) {
4970 let mut server = TestServer::start(executor.clone()).await;
4971 let client_a = server.create_client(cx_a, "user_a").await;
4972 let client_b = server.create_client(cx_b, "user_b").await;
4973 server
4974 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4975 .await;
4976 let active_call_a = cx_a.read(ActiveCall::global);
4977
4978 client_a
4979 .fs()
4980 .insert_tree(
4981 "/root-1",
4982 json!({
4983 "main.rs": "fn double(number: i32) -> i32 { number + number }",
4984 }),
4985 )
4986 .await;
4987
4988 let mut fake_language_servers = client_a
4989 .language_registry()
4990 .register_fake_lsp("Rust", Default::default());
4991 client_a.language_registry().add(rust_lang());
4992
4993 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4994 let project_id = active_call_a
4995 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4996 .await
4997 .unwrap();
4998 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4999
5000 // Open the file on client B.
5001 let open_b = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
5002 let buffer_b = cx_b.executor().spawn(open_b).await.unwrap();
5003
5004 // Request document highlights as the guest.
5005 let fake_language_server = fake_language_servers.next().await.unwrap();
5006 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
5007 |params, _| async move {
5008 assert_eq!(
5009 params
5010 .text_document_position_params
5011 .text_document
5012 .uri
5013 .as_str(),
5014 "file:///root-1/main.rs"
5015 );
5016 assert_eq!(
5017 params.text_document_position_params.position,
5018 lsp::Position::new(0, 34)
5019 );
5020 Ok(Some(vec![
5021 lsp::DocumentHighlight {
5022 kind: Some(lsp::DocumentHighlightKind::WRITE),
5023 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
5024 },
5025 lsp::DocumentHighlight {
5026 kind: Some(lsp::DocumentHighlightKind::READ),
5027 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
5028 },
5029 lsp::DocumentHighlight {
5030 kind: Some(lsp::DocumentHighlightKind::READ),
5031 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
5032 },
5033 ]))
5034 },
5035 );
5036
5037 let highlights = project_b
5038 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
5039 .await
5040 .unwrap();
5041
5042 buffer_b.read_with(cx_b, |buffer, _| {
5043 let snapshot = buffer.snapshot();
5044
5045 let highlights = highlights
5046 .into_iter()
5047 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
5048 .collect::<Vec<_>>();
5049 assert_eq!(
5050 highlights,
5051 &[
5052 (lsp::DocumentHighlightKind::WRITE, 10..16),
5053 (lsp::DocumentHighlightKind::READ, 32..38),
5054 (lsp::DocumentHighlightKind::READ, 41..47)
5055 ]
5056 )
5057 });
5058}
5059
5060#[gpui::test(iterations = 10)]
5061async fn test_lsp_hover(
5062 executor: BackgroundExecutor,
5063 cx_a: &mut TestAppContext,
5064 cx_b: &mut TestAppContext,
5065) {
5066 let mut server = TestServer::start(executor.clone()).await;
5067 let client_a = server.create_client(cx_a, "user_a").await;
5068 let client_b = server.create_client(cx_b, "user_b").await;
5069 server
5070 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5071 .await;
5072 let active_call_a = cx_a.read(ActiveCall::global);
5073
5074 client_a
5075 .fs()
5076 .insert_tree(
5077 "/root-1",
5078 json!({
5079 "main.rs": "use std::collections::HashMap;",
5080 }),
5081 )
5082 .await;
5083
5084 client_a.language_registry().add(rust_lang());
5085 let language_server_names = ["rust-analyzer", "CrabLang-ls"];
5086 let mut language_servers = [
5087 client_a.language_registry().register_fake_lsp(
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 client_a.language_registry().register_fake_lsp(
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
5111 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
5112 let project_id = active_call_a
5113 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5114 .await
5115 .unwrap();
5116 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5117
5118 // Open the file as the guest
5119 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
5120 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
5121
5122 let mut servers_with_hover_requests = HashMap::default();
5123 for i in 0..language_server_names.len() {
5124 let new_server = language_servers[i].next().await.unwrap_or_else(|| {
5125 panic!(
5126 "Failed to get language server #{i} with name {}",
5127 &language_server_names[i]
5128 )
5129 });
5130 let new_server_name = new_server.server.name();
5131 assert!(
5132 !servers_with_hover_requests.contains_key(&new_server_name),
5133 "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
5134 );
5135 match new_server_name.as_ref() {
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("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.join_remote_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("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.join_remote_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 = TestScreenCaptureSource::new();
6060 cx_a.set_screen_capture_sources(vec![display]);
6061 active_call_a
6062 .update(cx_a, |call, cx| {
6063 call.room()
6064 .unwrap()
6065 .update(cx, |room, cx| room.share_screen(cx))
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().next().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().next().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().next().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().next().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().next().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.join_remote_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 cx_a.update(context_servers::init);
6489 cx_b.update(context_servers::init);
6490 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
6491 let context_store_a = cx_a
6492 .update(|cx| {
6493 ContextStore::new(
6494 project_a.clone(),
6495 prompt_builder.clone(),
6496 Arc::new(SlashCommandWorkingSet::default()),
6497 Arc::new(ToolWorkingSet::default()),
6498 cx,
6499 )
6500 })
6501 .await
6502 .unwrap();
6503 let context_store_b = cx_b
6504 .update(|cx| {
6505 ContextStore::new(
6506 project_b.clone(),
6507 prompt_builder.clone(),
6508 Arc::new(SlashCommandWorkingSet::default()),
6509 Arc::new(ToolWorkingSet::default()),
6510 cx,
6511 )
6512 })
6513 .await
6514 .unwrap();
6515
6516 // Client A creates a new chats.
6517 let context_a = context_store_a.update(cx_a, |store, cx| store.create(cx));
6518 executor.run_until_parked();
6519
6520 // Client B retrieves host's contexts and joins one.
6521 let context_b = context_store_b
6522 .update(cx_b, |store, cx| {
6523 let host_contexts = store.host_contexts().to_vec();
6524 assert_eq!(host_contexts.len(), 1);
6525 store.open_remote_context(host_contexts[0].id.clone(), cx)
6526 })
6527 .await
6528 .unwrap();
6529
6530 // Host and guest make changes
6531 context_a.update(cx_a, |context, cx| {
6532 context.buffer().update(cx, |buffer, cx| {
6533 buffer.edit([(0..0, "Host change\n")], None, cx)
6534 })
6535 });
6536 context_b.update(cx_b, |context, cx| {
6537 context.buffer().update(cx, |buffer, cx| {
6538 buffer.edit([(0..0, "Guest change\n")], None, cx)
6539 })
6540 });
6541 executor.run_until_parked();
6542 assert_eq!(
6543 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6544 "Guest change\nHost change\n"
6545 );
6546 assert_eq!(
6547 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6548 "Guest change\nHost change\n"
6549 );
6550
6551 // Disconnect client A and make some changes while disconnected.
6552 server.disconnect_client(client_a.peer_id().unwrap());
6553 server.forbid_connections();
6554 context_a.update(cx_a, |context, cx| {
6555 context.buffer().update(cx, |buffer, cx| {
6556 buffer.edit([(0..0, "Host offline change\n")], None, cx)
6557 })
6558 });
6559 context_b.update(cx_b, |context, cx| {
6560 context.buffer().update(cx, |buffer, cx| {
6561 buffer.edit([(0..0, "Guest offline change\n")], None, cx)
6562 })
6563 });
6564 executor.run_until_parked();
6565 assert_eq!(
6566 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6567 "Host offline change\nGuest change\nHost change\n"
6568 );
6569 assert_eq!(
6570 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6571 "Guest offline change\nGuest change\nHost change\n"
6572 );
6573
6574 // Allow client A to reconnect and verify that contexts converge.
6575 server.allow_connections();
6576 executor.advance_clock(RECEIVE_TIMEOUT);
6577 assert_eq!(
6578 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6579 "Guest offline change\nHost offline change\nGuest change\nHost change\n"
6580 );
6581 assert_eq!(
6582 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6583 "Guest offline change\nHost offline change\nGuest change\nHost change\n"
6584 );
6585
6586 // Client A disconnects without being able to reconnect. Context B becomes readonly.
6587 server.forbid_connections();
6588 server.disconnect_client(client_a.peer_id().unwrap());
6589 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
6590 context_b.read_with(cx_b, |context, cx| {
6591 assert!(context.buffer().read(cx).read_only());
6592 });
6593}
6594
6595#[gpui::test]
6596async fn test_remote_git_branches(
6597 executor: BackgroundExecutor,
6598 cx_a: &mut TestAppContext,
6599 cx_b: &mut TestAppContext,
6600) {
6601 let mut server = TestServer::start(executor.clone()).await;
6602 let client_a = server.create_client(cx_a, "user_a").await;
6603 let client_b = server.create_client(cx_b, "user_b").await;
6604 server
6605 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6606 .await;
6607 let active_call_a = cx_a.read(ActiveCall::global);
6608
6609 client_a
6610 .fs()
6611 .insert_tree("/project", serde_json::json!({ ".git":{} }))
6612 .await;
6613 let branches = ["main", "dev", "feature-1"];
6614 client_a
6615 .fs()
6616 .insert_branches(Path::new("/project/.git"), &branches);
6617
6618 let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await;
6619 let project_id = active_call_a
6620 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6621 .await
6622 .unwrap();
6623 let project_b = client_b.join_remote_project(project_id, cx_b).await;
6624
6625 let root_path = ProjectPath::root_path(worktree_id);
6626 // Client A sees that a guest has joined.
6627 executor.run_until_parked();
6628
6629 let branches_b = cx_b
6630 .update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
6631 .await
6632 .unwrap();
6633
6634 let new_branch = branches[2];
6635
6636 let branches_b = branches_b
6637 .into_iter()
6638 .map(|branch| branch.name)
6639 .collect::<Vec<_>>();
6640
6641 assert_eq!(&branches_b, &branches);
6642
6643 cx_b.update(|cx| {
6644 project_b.update(cx, |project, cx| {
6645 project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
6646 })
6647 })
6648 .await
6649 .unwrap();
6650
6651 executor.run_until_parked();
6652
6653 let host_branch = cx_a.update(|cx| {
6654 project_a.update(cx, |project, cx| {
6655 project.worktree_store().update(cx, |worktree_store, cx| {
6656 worktree_store
6657 .current_branch(root_path.clone(), cx)
6658 .unwrap()
6659 })
6660 })
6661 });
6662
6663 assert_eq!(host_branch.as_ref(), branches[2]);
6664
6665 // Also try creating a new branch
6666 cx_b.update(|cx| {
6667 project_b.update(cx, |project, cx| {
6668 project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
6669 })
6670 })
6671 .await
6672 .unwrap();
6673
6674 executor.run_until_parked();
6675
6676 let host_branch = cx_a.update(|cx| {
6677 project_a.update(cx, |project, cx| {
6678 project.worktree_store().update(cx, |worktree_store, cx| {
6679 worktree_store.current_branch(root_path, cx).unwrap()
6680 })
6681 })
6682 });
6683
6684 assert_eq!(host_branch.as_ref(), "totally-new-branch");
6685}