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