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