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