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