1use crate::{
2 rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
3 tests::{
4 RoomParticipants, TestClient, TestServer, channel_id, following_tests::join_channel,
5 room_participants,
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 rust_lang, 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(cx).as_deref(),
2651 Some(staged_text.as_str())
2652 );
2653 assert_hunks(
2654 diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
2655 buffer,
2656 &diff.base_text_string(cx).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(cx).as_deref(),
2681 Some(staged_text.as_str())
2682 );
2683 assert_hunks(
2684 diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
2685 buffer,
2686 &diff.base_text_string(cx).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(cx).as_deref(),
2703 Some(committed_text.as_str())
2704 );
2705 assert_hunks(
2706 diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
2707 buffer,
2708 &diff.base_text_string(cx).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(cx).as_deref(),
2735 Some(new_staged_text.as_str())
2736 );
2737 assert_hunks(
2738 diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
2739 buffer,
2740 &diff.base_text_string(cx).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(cx).as_deref(),
2750 Some(new_staged_text.as_str())
2751 );
2752 assert_hunks(
2753 diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
2754 buffer,
2755 &diff.base_text_string(cx).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(cx).as_deref(),
2764 Some(new_committed_text.as_str())
2765 );
2766 assert_hunks(
2767 diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
2768 buffer,
2769 &diff.base_text_string(cx).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(cx).as_deref(),
2817 Some(staged_text.as_str())
2818 );
2819 assert_hunks(
2820 diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
2821 buffer,
2822 &diff.base_text_string(cx).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(cx).as_deref(),
2846 Some(staged_text.as_str())
2847 );
2848 assert_hunks(
2849 diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
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(cx).as_deref(),
2868 Some(new_staged_text.as_str())
2869 );
2870 assert_hunks(
2871 diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
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(cx).as_deref(),
2882 Some(new_staged_text.as_str())
2883 );
2884 assert_hunks(
2885 diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
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 executor.run_until_parked();
4362 fake_language_server
4363 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
4364 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4365 })
4366 .await
4367 .into_response()
4368 .unwrap();
4369 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4370 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4371 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
4372 lsp::WorkDoneProgressBegin {
4373 title: "Progress Began".into(),
4374 ..Default::default()
4375 },
4376 )),
4377 });
4378 for file_name in file_names {
4379 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4380 lsp::PublishDiagnosticsParams {
4381 uri: lsp::Uri::from_file_path(Path::new(path!("/test")).join(file_name)).unwrap(),
4382 version: None,
4383 diagnostics: vec![lsp::Diagnostic {
4384 severity: Some(lsp::DiagnosticSeverity::WARNING),
4385 source: Some("the-disk-based-diagnostics-source".into()),
4386 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
4387 message: "message one".to_string(),
4388 ..Default::default()
4389 }],
4390 },
4391 );
4392 }
4393 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4394 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4395 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
4396 lsp::WorkDoneProgressEnd { message: None },
4397 )),
4398 });
4399
4400 // When the "disk base diagnostics finished" message is received, the buffers'
4401 // diagnostics are expected to be present.
4402 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
4403 project_b.update(cx_b, {
4404 let project_b = project_b.clone();
4405 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
4406 move |_, cx| {
4407 cx.subscribe(&project_b, move |_, _, event, cx| {
4408 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
4409 disk_based_diagnostics_finished.store(true, SeqCst);
4410 for (buffer, _) in &guest_buffers {
4411 assert_eq!(
4412 buffer
4413 .read(cx)
4414 .snapshot()
4415 .diagnostics_in_range::<_, usize>(0..5, false)
4416 .count(),
4417 1,
4418 "expected a diagnostic for buffer {:?}",
4419 buffer.read(cx).file().unwrap().path(),
4420 );
4421 }
4422 }
4423 })
4424 .detach();
4425 }
4426 });
4427
4428 executor.run_until_parked();
4429 assert!(disk_based_diagnostics_finished.load(SeqCst));
4430}
4431
4432#[gpui::test(iterations = 10)]
4433async fn test_reloading_buffer_manually(
4434 executor: BackgroundExecutor,
4435 cx_a: &mut TestAppContext,
4436 cx_b: &mut TestAppContext,
4437) {
4438 let mut server = TestServer::start(executor.clone()).await;
4439 let client_a = server.create_client(cx_a, "user_a").await;
4440 let client_b = server.create_client(cx_b, "user_b").await;
4441 server
4442 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4443 .await;
4444 let active_call_a = cx_a.read(ActiveCall::global);
4445
4446 client_a
4447 .fs()
4448 .insert_tree(path!("/a"), json!({ "a.rs": "let one = 1;" }))
4449 .await;
4450 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
4451 let buffer_a = project_a
4452 .update(cx_a, |p, cx| {
4453 p.open_buffer((worktree_id, rel_path("a.rs")), cx)
4454 })
4455 .await
4456 .unwrap();
4457 let project_id = active_call_a
4458 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4459 .await
4460 .unwrap();
4461
4462 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4463
4464 let open_buffer = project_b.update(cx_b, |p, cx| {
4465 p.open_buffer((worktree_id, rel_path("a.rs")), cx)
4466 });
4467 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4468 buffer_b.update(cx_b, |buffer, cx| {
4469 buffer.edit([(4..7, "six")], None, cx);
4470 buffer.edit([(10..11, "6")], None, cx);
4471 assert_eq!(buffer.text(), "let six = 6;");
4472 assert!(buffer.is_dirty());
4473 assert!(!buffer.has_conflict());
4474 });
4475 executor.run_until_parked();
4476
4477 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4478
4479 client_a
4480 .fs()
4481 .save(
4482 path!("/a/a.rs").as_ref(),
4483 &Rope::from("let seven = 7;"),
4484 LineEnding::Unix,
4485 )
4486 .await
4487 .unwrap();
4488 executor.run_until_parked();
4489
4490 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4491
4492 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4493
4494 project_b
4495 .update(cx_b, |project, cx| {
4496 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4497 })
4498 .await
4499 .unwrap();
4500
4501 buffer_a.read_with(cx_a, |buffer, _| {
4502 assert_eq!(buffer.text(), "let seven = 7;");
4503 assert!(!buffer.is_dirty());
4504 assert!(!buffer.has_conflict());
4505 });
4506
4507 buffer_b.read_with(cx_b, |buffer, _| {
4508 assert_eq!(buffer.text(), "let seven = 7;");
4509 assert!(!buffer.is_dirty());
4510 assert!(!buffer.has_conflict());
4511 });
4512
4513 buffer_a.update(cx_a, |buffer, cx| {
4514 // Undoing on the host is a no-op when the reload was initiated by the guest.
4515 buffer.undo(cx);
4516 assert_eq!(buffer.text(), "let seven = 7;");
4517 assert!(!buffer.is_dirty());
4518 assert!(!buffer.has_conflict());
4519 });
4520 buffer_b.update(cx_b, |buffer, cx| {
4521 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4522 buffer.undo(cx);
4523 assert_eq!(buffer.text(), "let six = 6;");
4524 assert!(buffer.is_dirty());
4525 assert!(!buffer.has_conflict());
4526 });
4527}
4528
4529#[gpui::test(iterations = 10)]
4530async fn test_formatting_buffer(
4531 executor: BackgroundExecutor,
4532 cx_a: &mut TestAppContext,
4533 cx_b: &mut TestAppContext,
4534) {
4535 let mut server = TestServer::start(executor.clone()).await;
4536 let client_a = server.create_client(cx_a, "user_a").await;
4537 let client_b = server.create_client(cx_b, "user_b").await;
4538 server
4539 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4540 .await;
4541 let active_call_a = cx_a.read(ActiveCall::global);
4542
4543 client_a.language_registry().add(rust_lang());
4544 let mut fake_language_servers = client_a
4545 .language_registry()
4546 .register_fake_lsp("Rust", FakeLspAdapter::default());
4547
4548 // Here we insert a fake tree with a directory that exists on disk. This is needed
4549 // because later we'll invoke a command, which requires passing a working directory
4550 // that points to a valid location on disk.
4551 let directory = env::current_dir().unwrap();
4552 client_a
4553 .fs()
4554 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4555 .await;
4556 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4557 let project_id = active_call_a
4558 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4559 .await
4560 .unwrap();
4561 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4562
4563 let buffer_b = project_b
4564 .update(cx_b, |p, cx| {
4565 p.open_buffer((worktree_id, rel_path("a.rs")), cx)
4566 })
4567 .await
4568 .unwrap();
4569
4570 let _handle = project_b.update(cx_b, |project, cx| {
4571 project.register_buffer_with_language_servers(&buffer_b, cx)
4572 });
4573 let fake_language_server = fake_language_servers.next().await.unwrap();
4574 executor.run_until_parked();
4575 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(|_, _| async move {
4576 Ok(Some(vec![
4577 lsp::TextEdit {
4578 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4579 new_text: "h".to_string(),
4580 },
4581 lsp::TextEdit {
4582 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4583 new_text: "y".to_string(),
4584 },
4585 ]))
4586 });
4587
4588 project_b
4589 .update(cx_b, |project, cx| {
4590 project.format(
4591 HashSet::from_iter([buffer_b.clone()]),
4592 LspFormatTarget::Buffers,
4593 true,
4594 FormatTrigger::Save,
4595 cx,
4596 )
4597 })
4598 .await
4599 .unwrap();
4600
4601 // The edits from the LSP are applied, and a final newline is added.
4602 assert_eq!(
4603 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4604 "let honey = \"two\"\n"
4605 );
4606
4607 // There is no `awk` command on Windows.
4608 #[cfg(not(target_os = "windows"))]
4609 {
4610 // Ensure buffer can be formatted using an external command. Notice how the
4611 // host's configuration is honored as opposed to using the guest's settings.
4612 cx_a.update(|cx| {
4613 SettingsStore::update_global(cx, |store, cx| {
4614 store.update_user_settings(cx, |file| {
4615 file.project.all_languages.defaults.formatter =
4616 Some(FormatterList::Single(Formatter::External {
4617 command: "awk".into(),
4618 arguments: Some(
4619 vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
4620 ),
4621 }));
4622 });
4623 });
4624 });
4625
4626 executor.allow_parking();
4627 project_b
4628 .update(cx_b, |project, cx| {
4629 project.format(
4630 HashSet::from_iter([buffer_b.clone()]),
4631 LspFormatTarget::Buffers,
4632 true,
4633 FormatTrigger::Save,
4634 cx,
4635 )
4636 })
4637 .await
4638 .unwrap();
4639 assert_eq!(
4640 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4641 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4642 );
4643 }
4644}
4645
4646#[gpui::test(iterations = 10)]
4647async fn test_prettier_formatting_buffer(
4648 executor: BackgroundExecutor,
4649 cx_a: &mut TestAppContext,
4650 cx_b: &mut TestAppContext,
4651) {
4652 let mut server = TestServer::start(executor.clone()).await;
4653 let client_a = server.create_client(cx_a, "user_a").await;
4654 let client_b = server.create_client(cx_b, "user_b").await;
4655 server
4656 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4657 .await;
4658 let active_call_a = cx_a.read(ActiveCall::global);
4659
4660 let test_plugin = "test_plugin";
4661
4662 client_a.language_registry().add(Arc::new(Language::new(
4663 LanguageConfig {
4664 name: "TypeScript".into(),
4665 matcher: LanguageMatcher {
4666 path_suffixes: vec!["ts".to_string()],
4667 ..Default::default()
4668 },
4669 ..Default::default()
4670 },
4671 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
4672 )));
4673 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4674 "TypeScript",
4675 FakeLspAdapter {
4676 prettier_plugins: vec![test_plugin],
4677 ..Default::default()
4678 },
4679 );
4680
4681 // Here we insert a fake tree with a directory that exists on disk. This is needed
4682 // because later we'll invoke a command, which requires passing a working directory
4683 // that points to a valid location on disk.
4684 let directory = env::current_dir().unwrap();
4685 let buffer_text = "let one = \"two\"";
4686 client_a
4687 .fs()
4688 .insert_tree(&directory, json!({ "a.ts": buffer_text }))
4689 .await;
4690 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4691 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
4692 let open_buffer = project_a.update(cx_a, |p, cx| {
4693 p.open_buffer((worktree_id, rel_path("a.ts")), cx)
4694 });
4695 let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
4696
4697 let project_id = active_call_a
4698 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4699 .await
4700 .unwrap();
4701 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4702 let (buffer_b, _) = project_b
4703 .update(cx_b, |p, cx| {
4704 p.open_buffer_with_lsp((worktree_id, rel_path("a.ts")), cx)
4705 })
4706 .await
4707 .unwrap();
4708
4709 cx_a.update(|cx| {
4710 SettingsStore::update_global(cx, |store, cx| {
4711 store.update_user_settings(cx, |file| {
4712 file.project.all_languages.defaults.formatter = Some(FormatterList::default());
4713 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
4714 allowed: Some(true),
4715 ..Default::default()
4716 });
4717 });
4718 });
4719 });
4720 cx_b.update(|cx| {
4721 SettingsStore::update_global(cx, |store, cx| {
4722 store.update_user_settings(cx, |file| {
4723 file.project.all_languages.defaults.formatter = Some(FormatterList::Single(
4724 Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
4725 ));
4726 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
4727 allowed: Some(true),
4728 ..Default::default()
4729 });
4730 });
4731 });
4732 });
4733 let fake_language_server = fake_language_servers.next().await.unwrap();
4734 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(|_, _| async move {
4735 panic!(
4736 "Unexpected: prettier should be preferred since it's enabled and language supports it"
4737 )
4738 });
4739
4740 project_b
4741 .update(cx_b, |project, cx| {
4742 project.format(
4743 HashSet::from_iter([buffer_b.clone()]),
4744 LspFormatTarget::Buffers,
4745 true,
4746 FormatTrigger::Save,
4747 cx,
4748 )
4749 })
4750 .await
4751 .unwrap();
4752
4753 executor.run_until_parked();
4754 assert_eq!(
4755 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4756 buffer_text.to_string() + "\n" + prettier_format_suffix,
4757 "Prettier formatting was not applied to client buffer after client's request"
4758 );
4759
4760 project_a
4761 .update(cx_a, |project, cx| {
4762 project.format(
4763 HashSet::from_iter([buffer_a.clone()]),
4764 LspFormatTarget::Buffers,
4765 true,
4766 FormatTrigger::Manual,
4767 cx,
4768 )
4769 })
4770 .await
4771 .unwrap();
4772
4773 executor.run_until_parked();
4774 assert_eq!(
4775 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4776 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
4777 "Prettier formatting was not applied to client buffer after host's request"
4778 );
4779}
4780
4781#[gpui::test(iterations = 10)]
4782async fn test_definition(
4783 executor: BackgroundExecutor,
4784 cx_a: &mut TestAppContext,
4785 cx_b: &mut TestAppContext,
4786) {
4787 let mut server = TestServer::start(executor.clone()).await;
4788 let client_a = server.create_client(cx_a, "user_a").await;
4789 let client_b = server.create_client(cx_b, "user_b").await;
4790 server
4791 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4792 .await;
4793 let active_call_a = cx_a.read(ActiveCall::global);
4794
4795 let capabilities = lsp::ServerCapabilities {
4796 definition_provider: Some(OneOf::Left(true)),
4797 type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
4798 ..lsp::ServerCapabilities::default()
4799 };
4800 client_a.language_registry().add(rust_lang());
4801 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4802 "Rust",
4803 FakeLspAdapter {
4804 capabilities: capabilities.clone(),
4805 ..FakeLspAdapter::default()
4806 },
4807 );
4808 client_b.language_registry().add(rust_lang());
4809 client_b.language_registry().register_fake_lsp_adapter(
4810 "Rust",
4811 FakeLspAdapter {
4812 capabilities,
4813 ..FakeLspAdapter::default()
4814 },
4815 );
4816
4817 client_a
4818 .fs()
4819 .insert_tree(
4820 path!("/root"),
4821 json!({
4822 "dir-1": {
4823 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4824 },
4825 "dir-2": {
4826 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4827 "c.rs": "type T2 = usize;",
4828 }
4829 }),
4830 )
4831 .await;
4832 let (project_a, worktree_id) = client_a
4833 .build_local_project(path!("/root/dir-1"), cx_a)
4834 .await;
4835 let project_id = active_call_a
4836 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4837 .await
4838 .unwrap();
4839 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4840
4841 // Open the file on client B.
4842 let (buffer_b, _handle) = project_b
4843 .update(cx_b, |p, cx| {
4844 p.open_buffer_with_lsp((worktree_id, rel_path("a.rs")), cx)
4845 })
4846 .await
4847 .unwrap();
4848
4849 // Request the definition of a symbol as the guest.
4850 let fake_language_server = fake_language_servers.next().await.unwrap();
4851 fake_language_server.set_request_handler::<lsp::request::GotoDefinition, _, _>(
4852 |_, _| async move {
4853 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4854 lsp::Location::new(
4855 lsp::Uri::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
4856 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4857 ),
4858 )))
4859 },
4860 );
4861 cx_a.run_until_parked();
4862 cx_b.run_until_parked();
4863
4864 let definitions_1 = project_b
4865 .update(cx_b, |p, cx| p.definitions(&buffer_b, 23, cx))
4866 .await
4867 .unwrap()
4868 .unwrap();
4869 cx_b.read(|cx| {
4870 assert_eq!(
4871 definitions_1.len(),
4872 1,
4873 "Unexpected definitions: {definitions_1:?}"
4874 );
4875 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4876 let target_buffer = definitions_1[0].target.buffer.read(cx);
4877 assert_eq!(
4878 target_buffer.text(),
4879 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4880 );
4881 assert_eq!(
4882 definitions_1[0].target.range.to_point(target_buffer),
4883 Point::new(0, 6)..Point::new(0, 9)
4884 );
4885 });
4886
4887 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4888 // the previous call to `definition`.
4889 fake_language_server.set_request_handler::<lsp::request::GotoDefinition, _, _>(
4890 |_, _| async move {
4891 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4892 lsp::Location::new(
4893 lsp::Uri::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
4894 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4895 ),
4896 )))
4897 },
4898 );
4899
4900 let definitions_2 = project_b
4901 .update(cx_b, |p, cx| p.definitions(&buffer_b, 33, cx))
4902 .await
4903 .unwrap()
4904 .unwrap();
4905 cx_b.read(|cx| {
4906 assert_eq!(definitions_2.len(), 1);
4907 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4908 let target_buffer = definitions_2[0].target.buffer.read(cx);
4909 assert_eq!(
4910 target_buffer.text(),
4911 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4912 );
4913 assert_eq!(
4914 definitions_2[0].target.range.to_point(target_buffer),
4915 Point::new(1, 6)..Point::new(1, 11)
4916 );
4917 });
4918 assert_eq!(
4919 definitions_1[0].target.buffer,
4920 definitions_2[0].target.buffer
4921 );
4922
4923 fake_language_server.set_request_handler::<lsp::request::GotoTypeDefinition, _, _>(
4924 |req, _| async move {
4925 assert_eq!(
4926 req.text_document_position_params.position,
4927 lsp::Position::new(0, 7)
4928 );
4929 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4930 lsp::Location::new(
4931 lsp::Uri::from_file_path(path!("/root/dir-2/c.rs")).unwrap(),
4932 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4933 ),
4934 )))
4935 },
4936 );
4937
4938 let type_definitions = project_b
4939 .update(cx_b, |p, cx| p.type_definitions(&buffer_b, 7, cx))
4940 .await
4941 .unwrap()
4942 .unwrap();
4943 cx_b.read(|cx| {
4944 assert_eq!(
4945 type_definitions.len(),
4946 1,
4947 "Unexpected type definitions: {type_definitions:?}"
4948 );
4949 let target_buffer = type_definitions[0].target.buffer.read(cx);
4950 assert_eq!(target_buffer.text(), "type T2 = usize;");
4951 assert_eq!(
4952 type_definitions[0].target.range.to_point(target_buffer),
4953 Point::new(0, 5)..Point::new(0, 7)
4954 );
4955 });
4956}
4957
4958#[gpui::test(iterations = 10)]
4959async fn test_references(
4960 executor: BackgroundExecutor,
4961 cx_a: &mut TestAppContext,
4962 cx_b: &mut TestAppContext,
4963) {
4964 let mut server = TestServer::start(executor.clone()).await;
4965 let client_a = server.create_client(cx_a, "user_a").await;
4966 let client_b = server.create_client(cx_b, "user_b").await;
4967 server
4968 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4969 .await;
4970 let active_call_a = cx_a.read(ActiveCall::global);
4971
4972 let capabilities = lsp::ServerCapabilities {
4973 references_provider: Some(lsp::OneOf::Left(true)),
4974 ..lsp::ServerCapabilities::default()
4975 };
4976 client_a.language_registry().add(rust_lang());
4977 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4978 "Rust",
4979 FakeLspAdapter {
4980 name: "my-fake-lsp-adapter",
4981 capabilities: capabilities.clone(),
4982 ..FakeLspAdapter::default()
4983 },
4984 );
4985 client_b.language_registry().add(rust_lang());
4986 client_b.language_registry().register_fake_lsp_adapter(
4987 "Rust",
4988 FakeLspAdapter {
4989 name: "my-fake-lsp-adapter",
4990 capabilities,
4991 ..FakeLspAdapter::default()
4992 },
4993 );
4994
4995 client_a
4996 .fs()
4997 .insert_tree(
4998 path!("/root"),
4999 json!({
5000 "dir-1": {
5001 "one.rs": "const ONE: usize = 1;",
5002 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
5003 },
5004 "dir-2": {
5005 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
5006 }
5007 }),
5008 )
5009 .await;
5010 let (project_a, worktree_id) = client_a
5011 .build_local_project(path!("/root/dir-1"), cx_a)
5012 .await;
5013 let project_id = active_call_a
5014 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5015 .await
5016 .unwrap();
5017 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5018
5019 // Open the file on client B.
5020 let (buffer_b, _handle) = project_b
5021 .update(cx_b, |p, cx| {
5022 p.open_buffer_with_lsp((worktree_id, rel_path("one.rs")), cx)
5023 })
5024 .await
5025 .unwrap();
5026
5027 // Request references to a symbol as the guest.
5028 let fake_language_server = fake_language_servers.next().await.unwrap();
5029 let (lsp_response_tx, rx) = mpsc::unbounded::<Result<Option<Vec<lsp::Location>>>>();
5030 fake_language_server.set_request_handler::<lsp::request::References, _, _>({
5031 let rx = Arc::new(Mutex::new(Some(rx)));
5032 move |params, _| {
5033 assert_eq!(
5034 params.text_document_position.text_document.uri.as_str(),
5035 uri!("file:///root/dir-1/one.rs")
5036 );
5037 let rx = rx.clone();
5038 async move {
5039 let mut response_rx = rx.lock().take().unwrap();
5040 let result = response_rx.next().await.unwrap();
5041 *rx.lock() = Some(response_rx);
5042 result
5043 }
5044 }
5045 });
5046 cx_a.run_until_parked();
5047 cx_b.run_until_parked();
5048
5049 let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx));
5050
5051 // User is informed that a request is pending.
5052 executor.run_until_parked();
5053 project_b.read_with(cx_b, |project, cx| {
5054 let status = project.language_server_statuses(cx).next().unwrap().1;
5055 assert_eq!(status.name.0, "my-fake-lsp-adapter");
5056 assert_eq!(
5057 status.pending_work.values().next().unwrap().message,
5058 Some("Finding references...".into())
5059 );
5060 });
5061
5062 // Cause the language server to respond.
5063 lsp_response_tx
5064 .unbounded_send(Ok(Some(vec![
5065 lsp::Location {
5066 uri: lsp::Uri::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
5067 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
5068 },
5069 lsp::Location {
5070 uri: lsp::Uri::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
5071 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
5072 },
5073 lsp::Location {
5074 uri: lsp::Uri::from_file_path(path!("/root/dir-2/three.rs")).unwrap(),
5075 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
5076 },
5077 ])))
5078 .unwrap();
5079
5080 let references = references.await.unwrap().unwrap();
5081 executor.run_until_parked();
5082 project_b.read_with(cx_b, |project, cx| {
5083 // User is informed that a request is no longer pending.
5084 let status = project.language_server_statuses(cx).next().unwrap().1;
5085 assert!(status.pending_work.is_empty());
5086
5087 assert_eq!(references.len(), 3);
5088 assert_eq!(project.worktrees(cx).count(), 2);
5089
5090 let two_buffer = references[0].buffer.read(cx);
5091 let three_buffer = references[2].buffer.read(cx);
5092 assert_eq!(
5093 two_buffer.file().unwrap().path().as_ref(),
5094 rel_path("two.rs")
5095 );
5096 assert_eq!(references[1].buffer, references[0].buffer);
5097 assert_eq!(
5098 three_buffer.file().unwrap().full_path(cx),
5099 Path::new(path!("/root/dir-2/three.rs"))
5100 );
5101
5102 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
5103 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
5104 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
5105 });
5106
5107 let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx));
5108
5109 // User is informed that a request is pending.
5110 executor.run_until_parked();
5111 project_b.read_with(cx_b, |project, cx| {
5112 let status = project.language_server_statuses(cx).next().unwrap().1;
5113 assert_eq!(status.name.0, "my-fake-lsp-adapter");
5114 assert_eq!(
5115 status.pending_work.values().next().unwrap().message,
5116 Some("Finding references...".into())
5117 );
5118 });
5119
5120 // Cause the LSP request to fail.
5121 lsp_response_tx
5122 .unbounded_send(Err(anyhow!("can't find references")))
5123 .unwrap();
5124 assert_eq!(references.await.unwrap().unwrap(), []);
5125
5126 // User is informed that the request is no longer pending.
5127 executor.run_until_parked();
5128 project_b.read_with(cx_b, |project, cx| {
5129 let status = project.language_server_statuses(cx).next().unwrap().1;
5130 assert!(status.pending_work.is_empty());
5131 });
5132}
5133
5134#[gpui::test(iterations = 10)]
5135async fn test_project_search(
5136 executor: BackgroundExecutor,
5137 cx_a: &mut TestAppContext,
5138 cx_b: &mut TestAppContext,
5139) {
5140 let mut server = TestServer::start(executor.clone()).await;
5141 let client_a = server.create_client(cx_a, "user_a").await;
5142 let client_b = server.create_client(cx_b, "user_b").await;
5143 server
5144 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5145 .await;
5146 let active_call_a = cx_a.read(ActiveCall::global);
5147
5148 client_a
5149 .fs()
5150 .insert_tree(
5151 "/root",
5152 json!({
5153 "dir-1": {
5154 "a": "hello world",
5155 "b": "goodnight moon",
5156 "c": "a world of goo",
5157 "d": "world champion of clown world",
5158 },
5159 "dir-2": {
5160 "e": "disney world is fun",
5161 }
5162 }),
5163 )
5164 .await;
5165 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
5166 let (worktree_2, _) = project_a
5167 .update(cx_a, |p, cx| {
5168 p.find_or_create_worktree("/root/dir-2", true, cx)
5169 })
5170 .await
5171 .unwrap();
5172 worktree_2
5173 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
5174 .await;
5175 let project_id = active_call_a
5176 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5177 .await
5178 .unwrap();
5179
5180 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5181
5182 // Perform a search as the guest.
5183 let mut results = HashMap::default();
5184 let search_rx = project_b.update(cx_b, |project, cx| {
5185 project.search(
5186 SearchQuery::text(
5187 "world",
5188 false,
5189 false,
5190 false,
5191 Default::default(),
5192 Default::default(),
5193 false,
5194 None,
5195 )
5196 .unwrap(),
5197 cx,
5198 )
5199 });
5200 while let Ok(result) = search_rx.rx.recv().await {
5201 match result {
5202 SearchResult::Buffer { buffer, ranges } => {
5203 results.entry(buffer).or_insert(ranges);
5204 }
5205 SearchResult::LimitReached => {
5206 panic!(
5207 "Unexpectedly reached search limit in tests. If you do want to assert limit-reached, change this panic call."
5208 )
5209 }
5210 };
5211 }
5212
5213 let mut ranges_by_path = results
5214 .into_iter()
5215 .map(|(buffer, ranges)| {
5216 buffer.read_with(cx_b, |buffer, cx| {
5217 let path = buffer.file().unwrap().full_path(cx);
5218 let offset_ranges = ranges
5219 .into_iter()
5220 .map(|range| range.to_offset(buffer))
5221 .collect::<Vec<_>>();
5222 (path, offset_ranges)
5223 })
5224 })
5225 .collect::<Vec<_>>();
5226 ranges_by_path.sort_by_key(|(path, _)| path.clone());
5227
5228 assert_eq!(
5229 ranges_by_path,
5230 &[
5231 (PathBuf::from("dir-1/a"), vec![6..11]),
5232 (PathBuf::from("dir-1/c"), vec![2..7]),
5233 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
5234 (PathBuf::from("dir-2/e"), vec![7..12]),
5235 ]
5236 );
5237}
5238
5239#[gpui::test(iterations = 10)]
5240async fn test_document_highlights(
5241 executor: BackgroundExecutor,
5242 cx_a: &mut TestAppContext,
5243 cx_b: &mut TestAppContext,
5244) {
5245 let mut server = TestServer::start(executor.clone()).await;
5246 let client_a = server.create_client(cx_a, "user_a").await;
5247 let client_b = server.create_client(cx_b, "user_b").await;
5248 server
5249 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5250 .await;
5251 let active_call_a = cx_a.read(ActiveCall::global);
5252
5253 client_a
5254 .fs()
5255 .insert_tree(
5256 path!("/root-1"),
5257 json!({
5258 "main.rs": "fn double(number: i32) -> i32 { number + number }",
5259 }),
5260 )
5261 .await;
5262
5263 client_a.language_registry().add(rust_lang());
5264 let capabilities = lsp::ServerCapabilities {
5265 document_highlight_provider: Some(lsp::OneOf::Left(true)),
5266 ..lsp::ServerCapabilities::default()
5267 };
5268 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
5269 "Rust",
5270 FakeLspAdapter {
5271 capabilities: capabilities.clone(),
5272 ..FakeLspAdapter::default()
5273 },
5274 );
5275 client_b.language_registry().add(rust_lang());
5276 client_b.language_registry().register_fake_lsp_adapter(
5277 "Rust",
5278 FakeLspAdapter {
5279 capabilities,
5280 ..FakeLspAdapter::default()
5281 },
5282 );
5283
5284 let (project_a, worktree_id) = client_a.build_local_project(path!("/root-1"), cx_a).await;
5285 let project_id = active_call_a
5286 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5287 .await
5288 .unwrap();
5289 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5290
5291 // Open the file on client B.
5292 let (buffer_b, _handle) = project_b
5293 .update(cx_b, |p, cx| {
5294 p.open_buffer_with_lsp((worktree_id, rel_path("main.rs")), cx)
5295 })
5296 .await
5297 .unwrap();
5298
5299 // Request document highlights as the guest.
5300 let fake_language_server = fake_language_servers.next().await.unwrap();
5301 fake_language_server.set_request_handler::<lsp::request::DocumentHighlightRequest, _, _>(
5302 |params, _| async move {
5303 assert_eq!(
5304 params
5305 .text_document_position_params
5306 .text_document
5307 .uri
5308 .as_str(),
5309 uri!("file:///root-1/main.rs")
5310 );
5311 assert_eq!(
5312 params.text_document_position_params.position,
5313 lsp::Position::new(0, 34)
5314 );
5315 Ok(Some(vec![
5316 lsp::DocumentHighlight {
5317 kind: Some(lsp::DocumentHighlightKind::WRITE),
5318 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
5319 },
5320 lsp::DocumentHighlight {
5321 kind: Some(lsp::DocumentHighlightKind::READ),
5322 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
5323 },
5324 lsp::DocumentHighlight {
5325 kind: Some(lsp::DocumentHighlightKind::READ),
5326 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
5327 },
5328 ]))
5329 },
5330 );
5331 cx_a.run_until_parked();
5332 cx_b.run_until_parked();
5333
5334 let highlights = project_b
5335 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
5336 .await
5337 .unwrap();
5338
5339 buffer_b.read_with(cx_b, |buffer, _| {
5340 let snapshot = buffer.snapshot();
5341
5342 let highlights = highlights
5343 .into_iter()
5344 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
5345 .collect::<Vec<_>>();
5346 assert_eq!(
5347 highlights,
5348 &[
5349 (lsp::DocumentHighlightKind::WRITE, 10..16),
5350 (lsp::DocumentHighlightKind::READ, 32..38),
5351 (lsp::DocumentHighlightKind::READ, 41..47)
5352 ]
5353 )
5354 });
5355}
5356
5357#[gpui::test(iterations = 10)]
5358async fn test_lsp_hover(
5359 executor: BackgroundExecutor,
5360 cx_a: &mut TestAppContext,
5361 cx_b: &mut TestAppContext,
5362) {
5363 let mut server = TestServer::start(executor.clone()).await;
5364 let client_a = server.create_client(cx_a, "user_a").await;
5365 let client_b = server.create_client(cx_b, "user_b").await;
5366 server
5367 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5368 .await;
5369 let active_call_a = cx_a.read(ActiveCall::global);
5370
5371 client_a
5372 .fs()
5373 .insert_tree(
5374 path!("/root-1"),
5375 json!({
5376 "main.rs": "use std::collections::HashMap;",
5377 }),
5378 )
5379 .await;
5380
5381 client_a.language_registry().add(rust_lang());
5382 let language_server_names = ["rust-analyzer", "CrabLang-ls"];
5383 let capabilities_1 = lsp::ServerCapabilities {
5384 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5385 ..lsp::ServerCapabilities::default()
5386 };
5387 let capabilities_2 = lsp::ServerCapabilities {
5388 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5389 ..lsp::ServerCapabilities::default()
5390 };
5391 let mut language_servers = [
5392 client_a.language_registry().register_fake_lsp(
5393 "Rust",
5394 FakeLspAdapter {
5395 name: language_server_names[0],
5396 capabilities: capabilities_1.clone(),
5397 ..FakeLspAdapter::default()
5398 },
5399 ),
5400 client_a.language_registry().register_fake_lsp(
5401 "Rust",
5402 FakeLspAdapter {
5403 name: language_server_names[1],
5404 capabilities: capabilities_2.clone(),
5405 ..FakeLspAdapter::default()
5406 },
5407 ),
5408 ];
5409 client_b.language_registry().add(rust_lang());
5410 client_b.language_registry().register_fake_lsp_adapter(
5411 "Rust",
5412 FakeLspAdapter {
5413 name: language_server_names[0],
5414 capabilities: capabilities_1,
5415 ..FakeLspAdapter::default()
5416 },
5417 );
5418 client_b.language_registry().register_fake_lsp_adapter(
5419 "Rust",
5420 FakeLspAdapter {
5421 name: language_server_names[1],
5422 capabilities: capabilities_2,
5423 ..FakeLspAdapter::default()
5424 },
5425 );
5426
5427 let (project_a, worktree_id) = client_a.build_local_project(path!("/root-1"), cx_a).await;
5428 let project_id = active_call_a
5429 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5430 .await
5431 .unwrap();
5432 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5433
5434 // Open the file as the guest
5435 let (buffer_b, _handle) = project_b
5436 .update(cx_b, |p, cx| {
5437 p.open_buffer_with_lsp((worktree_id, rel_path("main.rs")), cx)
5438 })
5439 .await
5440 .unwrap();
5441
5442 let mut servers_with_hover_requests = HashMap::default();
5443 for i in 0..language_server_names.len() {
5444 let new_server = language_servers[i].next().await.unwrap_or_else(|| {
5445 panic!(
5446 "Failed to get language server #{i} with name {}",
5447 &language_server_names[i]
5448 )
5449 });
5450 let new_server_name = new_server.server.name();
5451 assert!(
5452 !servers_with_hover_requests.contains_key(&new_server_name),
5453 "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
5454 );
5455 match new_server_name.as_ref() {
5456 "CrabLang-ls" => {
5457 servers_with_hover_requests.insert(
5458 new_server_name.clone(),
5459 new_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
5460 move |params, _| {
5461 assert_eq!(
5462 params
5463 .text_document_position_params
5464 .text_document
5465 .uri
5466 .as_str(),
5467 uri!("file:///root-1/main.rs")
5468 );
5469 let name = new_server_name.clone();
5470 async move {
5471 Ok(Some(lsp::Hover {
5472 contents: lsp::HoverContents::Scalar(
5473 lsp::MarkedString::String(format!("{name} hover")),
5474 ),
5475 range: None,
5476 }))
5477 }
5478 },
5479 ),
5480 );
5481 }
5482 "rust-analyzer" => {
5483 servers_with_hover_requests.insert(
5484 new_server_name.clone(),
5485 new_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
5486 |params, _| async move {
5487 assert_eq!(
5488 params
5489 .text_document_position_params
5490 .text_document
5491 .uri
5492 .as_str(),
5493 uri!("file:///root-1/main.rs")
5494 );
5495 assert_eq!(
5496 params.text_document_position_params.position,
5497 lsp::Position::new(0, 22)
5498 );
5499 Ok(Some(lsp::Hover {
5500 contents: lsp::HoverContents::Array(vec![
5501 lsp::MarkedString::String("Test hover content.".to_string()),
5502 lsp::MarkedString::LanguageString(lsp::LanguageString {
5503 language: "Rust".to_string(),
5504 value: "let foo = 42;".to_string(),
5505 }),
5506 ]),
5507 range: Some(lsp::Range::new(
5508 lsp::Position::new(0, 22),
5509 lsp::Position::new(0, 29),
5510 )),
5511 }))
5512 },
5513 ),
5514 );
5515 }
5516 unexpected => panic!("Unexpected server name: {unexpected}"),
5517 }
5518 }
5519 cx_a.run_until_parked();
5520 cx_b.run_until_parked();
5521
5522 // Request hover information as the guest.
5523 let mut hovers = project_b
5524 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
5525 .await
5526 .unwrap();
5527 assert_eq!(
5528 hovers.len(),
5529 2,
5530 "Expected two hovers from both language servers, but got: {hovers:?}"
5531 );
5532
5533 let _: Vec<()> = futures::future::join_all(servers_with_hover_requests.into_values().map(
5534 |mut hover_request| async move {
5535 hover_request
5536 .next()
5537 .await
5538 .expect("All hover requests should have been triggered")
5539 },
5540 ))
5541 .await;
5542
5543 hovers.sort_by_key(|hover| hover.contents.len());
5544 let first_hover = hovers.first().cloned().unwrap();
5545 assert_eq!(
5546 first_hover.contents,
5547 vec![project::HoverBlock {
5548 text: "CrabLang-ls hover".to_string(),
5549 kind: HoverBlockKind::Markdown,
5550 },]
5551 );
5552 let second_hover = hovers.last().cloned().unwrap();
5553 assert_eq!(
5554 second_hover.contents,
5555 vec![
5556 project::HoverBlock {
5557 text: "Test hover content.".to_string(),
5558 kind: HoverBlockKind::Markdown,
5559 },
5560 project::HoverBlock {
5561 text: "let foo = 42;".to_string(),
5562 kind: HoverBlockKind::Code {
5563 language: "Rust".to_string()
5564 },
5565 }
5566 ]
5567 );
5568 buffer_b.read_with(cx_b, |buffer, _| {
5569 let snapshot = buffer.snapshot();
5570 assert_eq!(second_hover.range.unwrap().to_offset(&snapshot), 22..29);
5571 });
5572}
5573
5574#[gpui::test(iterations = 10)]
5575async fn test_project_symbols(
5576 executor: BackgroundExecutor,
5577 cx_a: &mut TestAppContext,
5578 cx_b: &mut TestAppContext,
5579) {
5580 let mut server = TestServer::start(executor.clone()).await;
5581 let client_a = server.create_client(cx_a, "user_a").await;
5582 let client_b = server.create_client(cx_b, "user_b").await;
5583 server
5584 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5585 .await;
5586 let active_call_a = cx_a.read(ActiveCall::global);
5587
5588 client_a.language_registry().add(rust_lang());
5589 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
5590 "Rust",
5591 FakeLspAdapter {
5592 capabilities: lsp::ServerCapabilities {
5593 workspace_symbol_provider: Some(OneOf::Left(true)),
5594 ..Default::default()
5595 },
5596 ..Default::default()
5597 },
5598 );
5599
5600 client_a
5601 .fs()
5602 .insert_tree(
5603 path!("/code"),
5604 json!({
5605 "crate-1": {
5606 "one.rs": "const ONE: usize = 1;",
5607 },
5608 "crate-2": {
5609 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
5610 },
5611 "private": {
5612 "passwords.txt": "the-password",
5613 }
5614 }),
5615 )
5616 .await;
5617 let (project_a, worktree_id) = client_a
5618 .build_local_project(path!("/code/crate-1"), cx_a)
5619 .await;
5620 let project_id = active_call_a
5621 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5622 .await
5623 .unwrap();
5624 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5625
5626 // Cause the language server to start.
5627 let _buffer = project_b
5628 .update(cx_b, |p, cx| {
5629 p.open_buffer_with_lsp((worktree_id, rel_path("one.rs")), cx)
5630 })
5631 .await
5632 .unwrap();
5633
5634 let fake_language_server = fake_language_servers.next().await.unwrap();
5635 executor.run_until_parked();
5636 fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
5637 |_, _| async move {
5638 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
5639 #[allow(deprecated)]
5640 lsp::SymbolInformation {
5641 name: "TWO".into(),
5642 location: lsp::Location {
5643 uri: lsp::Uri::from_file_path(path!("/code/crate-2/two.rs")).unwrap(),
5644 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5645 },
5646 kind: lsp::SymbolKind::CONSTANT,
5647 tags: None,
5648 container_name: None,
5649 deprecated: None,
5650 },
5651 ])))
5652 },
5653 );
5654
5655 // Request the definition of a symbol as the guest.
5656 let symbols = project_b
5657 .update(cx_b, |p, cx| p.symbols("two", cx))
5658 .await
5659 .unwrap();
5660 assert_eq!(symbols.len(), 1);
5661 assert_eq!(symbols[0].name, "TWO");
5662
5663 // Open one of the returned symbols.
5664 let buffer_b_2 = project_b
5665 .update(cx_b, |project, cx| {
5666 project.open_buffer_for_symbol(&symbols[0], cx)
5667 })
5668 .await
5669 .unwrap();
5670
5671 buffer_b_2.read_with(cx_b, |buffer, cx| {
5672 assert_eq!(
5673 buffer.file().unwrap().full_path(cx),
5674 Path::new(path!("/code/crate-2/two.rs"))
5675 );
5676 });
5677
5678 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
5679 let mut fake_symbol = symbols[0].clone();
5680 fake_symbol.path = SymbolLocation::OutsideProject {
5681 abs_path: Path::new(path!("/code/secrets")).into(),
5682 signature: [0x17; 32],
5683 };
5684 let error = project_b
5685 .update(cx_b, |project, cx| {
5686 project.open_buffer_for_symbol(&fake_symbol, cx)
5687 })
5688 .await
5689 .unwrap_err();
5690 assert!(error.to_string().contains("invalid symbol signature"));
5691}
5692
5693#[gpui::test(iterations = 10)]
5694async fn test_open_buffer_while_getting_definition_pointing_to_it(
5695 executor: BackgroundExecutor,
5696 cx_a: &mut TestAppContext,
5697 cx_b: &mut TestAppContext,
5698 mut rng: StdRng,
5699) {
5700 let mut server = TestServer::start(executor.clone()).await;
5701 let client_a = server.create_client(cx_a, "user_a").await;
5702 let client_b = server.create_client(cx_b, "user_b").await;
5703 server
5704 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5705 .await;
5706 let active_call_a = cx_a.read(ActiveCall::global);
5707
5708 let capabilities = lsp::ServerCapabilities {
5709 definition_provider: Some(OneOf::Left(true)),
5710 ..lsp::ServerCapabilities::default()
5711 };
5712 client_a.language_registry().add(rust_lang());
5713 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
5714 "Rust",
5715 FakeLspAdapter {
5716 capabilities: capabilities.clone(),
5717 ..FakeLspAdapter::default()
5718 },
5719 );
5720 client_b.language_registry().add(rust_lang());
5721 client_b.language_registry().register_fake_lsp_adapter(
5722 "Rust",
5723 FakeLspAdapter {
5724 capabilities,
5725 ..FakeLspAdapter::default()
5726 },
5727 );
5728
5729 client_a
5730 .fs()
5731 .insert_tree(
5732 path!("/root"),
5733 json!({
5734 "a.rs": "const ONE: usize = b::TWO;",
5735 "b.rs": "const TWO: usize = 2",
5736 }),
5737 )
5738 .await;
5739 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
5740 let project_id = active_call_a
5741 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5742 .await
5743 .unwrap();
5744 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5745
5746 let (buffer_b1, _lsp) = project_b
5747 .update(cx_b, |p, cx| {
5748 p.open_buffer_with_lsp((worktree_id, rel_path("a.rs")), cx)
5749 })
5750 .await
5751 .unwrap();
5752
5753 let fake_language_server = fake_language_servers.next().await.unwrap();
5754 fake_language_server.set_request_handler::<lsp::request::GotoDefinition, _, _>(
5755 |_, _| async move {
5756 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
5757 lsp::Location::new(
5758 lsp::Uri::from_file_path(path!("/root/b.rs")).unwrap(),
5759 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5760 ),
5761 )))
5762 },
5763 );
5764
5765 let definitions;
5766 let buffer_b2;
5767 if rng.random() {
5768 cx_a.run_until_parked();
5769 cx_b.run_until_parked();
5770 definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx));
5771 (buffer_b2, _) = project_b
5772 .update(cx_b, |p, cx| {
5773 p.open_buffer_with_lsp((worktree_id, rel_path("b.rs")), cx)
5774 })
5775 .await
5776 .unwrap();
5777 } else {
5778 (buffer_b2, _) = project_b
5779 .update(cx_b, |p, cx| {
5780 p.open_buffer_with_lsp((worktree_id, rel_path("b.rs")), cx)
5781 })
5782 .await
5783 .unwrap();
5784 cx_a.run_until_parked();
5785 cx_b.run_until_parked();
5786 definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx));
5787 }
5788
5789 let definitions = definitions.await.unwrap().unwrap();
5790 assert_eq!(
5791 definitions.len(),
5792 1,
5793 "Unexpected definitions: {definitions:?}"
5794 );
5795 assert_eq!(definitions[0].target.buffer, buffer_b2);
5796}
5797
5798#[gpui::test(iterations = 10)]
5799async fn test_contacts(
5800 executor: BackgroundExecutor,
5801 cx_a: &mut TestAppContext,
5802 cx_b: &mut TestAppContext,
5803 cx_c: &mut TestAppContext,
5804 cx_d: &mut TestAppContext,
5805) {
5806 let mut server = TestServer::start(executor.clone()).await;
5807 let client_a = server.create_client(cx_a, "user_a").await;
5808 let client_b = server.create_client(cx_b, "user_b").await;
5809 let client_c = server.create_client(cx_c, "user_c").await;
5810 let client_d = server.create_client(cx_d, "user_d").await;
5811 server
5812 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5813 .await;
5814 let active_call_a = cx_a.read(ActiveCall::global);
5815 let active_call_b = cx_b.read(ActiveCall::global);
5816 let active_call_c = cx_c.read(ActiveCall::global);
5817 let _active_call_d = cx_d.read(ActiveCall::global);
5818
5819 executor.run_until_parked();
5820 assert_eq!(
5821 contacts(&client_a, cx_a),
5822 [
5823 ("user_b".to_string(), "online", "free"),
5824 ("user_c".to_string(), "online", "free")
5825 ]
5826 );
5827 assert_eq!(
5828 contacts(&client_b, cx_b),
5829 [
5830 ("user_a".to_string(), "online", "free"),
5831 ("user_c".to_string(), "online", "free")
5832 ]
5833 );
5834 assert_eq!(
5835 contacts(&client_c, cx_c),
5836 [
5837 ("user_a".to_string(), "online", "free"),
5838 ("user_b".to_string(), "online", "free")
5839 ]
5840 );
5841 assert_eq!(contacts(&client_d, cx_d), []);
5842
5843 server.disconnect_client(client_c.peer_id().unwrap());
5844 server.forbid_connections();
5845 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5846 assert_eq!(
5847 contacts(&client_a, cx_a),
5848 [
5849 ("user_b".to_string(), "online", "free"),
5850 ("user_c".to_string(), "offline", "free")
5851 ]
5852 );
5853 assert_eq!(
5854 contacts(&client_b, cx_b),
5855 [
5856 ("user_a".to_string(), "online", "free"),
5857 ("user_c".to_string(), "offline", "free")
5858 ]
5859 );
5860 assert_eq!(contacts(&client_c, cx_c), []);
5861 assert_eq!(contacts(&client_d, cx_d), []);
5862
5863 server.allow_connections();
5864 client_c
5865 .connect(false, &cx_c.to_async())
5866 .await
5867 .into_response()
5868 .unwrap();
5869
5870 executor.run_until_parked();
5871 assert_eq!(
5872 contacts(&client_a, cx_a),
5873 [
5874 ("user_b".to_string(), "online", "free"),
5875 ("user_c".to_string(), "online", "free")
5876 ]
5877 );
5878 assert_eq!(
5879 contacts(&client_b, cx_b),
5880 [
5881 ("user_a".to_string(), "online", "free"),
5882 ("user_c".to_string(), "online", "free")
5883 ]
5884 );
5885 assert_eq!(
5886 contacts(&client_c, cx_c),
5887 [
5888 ("user_a".to_string(), "online", "free"),
5889 ("user_b".to_string(), "online", "free")
5890 ]
5891 );
5892 assert_eq!(contacts(&client_d, cx_d), []);
5893
5894 active_call_a
5895 .update(cx_a, |call, cx| {
5896 call.invite(client_b.user_id().unwrap(), None, cx)
5897 })
5898 .await
5899 .unwrap();
5900 executor.run_until_parked();
5901 assert_eq!(
5902 contacts(&client_a, cx_a),
5903 [
5904 ("user_b".to_string(), "online", "busy"),
5905 ("user_c".to_string(), "online", "free")
5906 ]
5907 );
5908 assert_eq!(
5909 contacts(&client_b, cx_b),
5910 [
5911 ("user_a".to_string(), "online", "busy"),
5912 ("user_c".to_string(), "online", "free")
5913 ]
5914 );
5915 assert_eq!(
5916 contacts(&client_c, cx_c),
5917 [
5918 ("user_a".to_string(), "online", "busy"),
5919 ("user_b".to_string(), "online", "busy")
5920 ]
5921 );
5922 assert_eq!(contacts(&client_d, cx_d), []);
5923
5924 // Client B and client D become contacts while client B is being called.
5925 server
5926 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5927 .await;
5928 executor.run_until_parked();
5929 assert_eq!(
5930 contacts(&client_a, cx_a),
5931 [
5932 ("user_b".to_string(), "online", "busy"),
5933 ("user_c".to_string(), "online", "free")
5934 ]
5935 );
5936 assert_eq!(
5937 contacts(&client_b, cx_b),
5938 [
5939 ("user_a".to_string(), "online", "busy"),
5940 ("user_c".to_string(), "online", "free"),
5941 ("user_d".to_string(), "online", "free"),
5942 ]
5943 );
5944 assert_eq!(
5945 contacts(&client_c, cx_c),
5946 [
5947 ("user_a".to_string(), "online", "busy"),
5948 ("user_b".to_string(), "online", "busy")
5949 ]
5950 );
5951 assert_eq!(
5952 contacts(&client_d, cx_d),
5953 [("user_b".to_string(), "online", "busy")]
5954 );
5955
5956 active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
5957 executor.run_until_parked();
5958 assert_eq!(
5959 contacts(&client_a, cx_a),
5960 [
5961 ("user_b".to_string(), "online", "free"),
5962 ("user_c".to_string(), "online", "free")
5963 ]
5964 );
5965 assert_eq!(
5966 contacts(&client_b, cx_b),
5967 [
5968 ("user_a".to_string(), "online", "free"),
5969 ("user_c".to_string(), "online", "free"),
5970 ("user_d".to_string(), "online", "free")
5971 ]
5972 );
5973 assert_eq!(
5974 contacts(&client_c, cx_c),
5975 [
5976 ("user_a".to_string(), "online", "free"),
5977 ("user_b".to_string(), "online", "free")
5978 ]
5979 );
5980 assert_eq!(
5981 contacts(&client_d, cx_d),
5982 [("user_b".to_string(), "online", "free")]
5983 );
5984
5985 active_call_c
5986 .update(cx_c, |call, cx| {
5987 call.invite(client_a.user_id().unwrap(), None, cx)
5988 })
5989 .await
5990 .unwrap();
5991 executor.run_until_parked();
5992 assert_eq!(
5993 contacts(&client_a, cx_a),
5994 [
5995 ("user_b".to_string(), "online", "free"),
5996 ("user_c".to_string(), "online", "busy")
5997 ]
5998 );
5999 assert_eq!(
6000 contacts(&client_b, cx_b),
6001 [
6002 ("user_a".to_string(), "online", "busy"),
6003 ("user_c".to_string(), "online", "busy"),
6004 ("user_d".to_string(), "online", "free")
6005 ]
6006 );
6007 assert_eq!(
6008 contacts(&client_c, cx_c),
6009 [
6010 ("user_a".to_string(), "online", "busy"),
6011 ("user_b".to_string(), "online", "free")
6012 ]
6013 );
6014 assert_eq!(
6015 contacts(&client_d, cx_d),
6016 [("user_b".to_string(), "online", "free")]
6017 );
6018
6019 active_call_a
6020 .update(cx_a, |call, cx| call.accept_incoming(cx))
6021 .await
6022 .unwrap();
6023 executor.run_until_parked();
6024 assert_eq!(
6025 contacts(&client_a, cx_a),
6026 [
6027 ("user_b".to_string(), "online", "free"),
6028 ("user_c".to_string(), "online", "busy")
6029 ]
6030 );
6031 assert_eq!(
6032 contacts(&client_b, cx_b),
6033 [
6034 ("user_a".to_string(), "online", "busy"),
6035 ("user_c".to_string(), "online", "busy"),
6036 ("user_d".to_string(), "online", "free")
6037 ]
6038 );
6039 assert_eq!(
6040 contacts(&client_c, cx_c),
6041 [
6042 ("user_a".to_string(), "online", "busy"),
6043 ("user_b".to_string(), "online", "free")
6044 ]
6045 );
6046 assert_eq!(
6047 contacts(&client_d, cx_d),
6048 [("user_b".to_string(), "online", "free")]
6049 );
6050
6051 active_call_a
6052 .update(cx_a, |call, cx| {
6053 call.invite(client_b.user_id().unwrap(), None, cx)
6054 })
6055 .await
6056 .unwrap();
6057 executor.run_until_parked();
6058 assert_eq!(
6059 contacts(&client_a, cx_a),
6060 [
6061 ("user_b".to_string(), "online", "busy"),
6062 ("user_c".to_string(), "online", "busy")
6063 ]
6064 );
6065 assert_eq!(
6066 contacts(&client_b, cx_b),
6067 [
6068 ("user_a".to_string(), "online", "busy"),
6069 ("user_c".to_string(), "online", "busy"),
6070 ("user_d".to_string(), "online", "free")
6071 ]
6072 );
6073 assert_eq!(
6074 contacts(&client_c, cx_c),
6075 [
6076 ("user_a".to_string(), "online", "busy"),
6077 ("user_b".to_string(), "online", "busy")
6078 ]
6079 );
6080 assert_eq!(
6081 contacts(&client_d, cx_d),
6082 [("user_b".to_string(), "online", "busy")]
6083 );
6084
6085 active_call_a
6086 .update(cx_a, |call, cx| call.hang_up(cx))
6087 .await
6088 .unwrap();
6089 executor.run_until_parked();
6090 assert_eq!(
6091 contacts(&client_a, cx_a),
6092 [
6093 ("user_b".to_string(), "online", "free"),
6094 ("user_c".to_string(), "online", "free")
6095 ]
6096 );
6097 assert_eq!(
6098 contacts(&client_b, cx_b),
6099 [
6100 ("user_a".to_string(), "online", "free"),
6101 ("user_c".to_string(), "online", "free"),
6102 ("user_d".to_string(), "online", "free")
6103 ]
6104 );
6105 assert_eq!(
6106 contacts(&client_c, cx_c),
6107 [
6108 ("user_a".to_string(), "online", "free"),
6109 ("user_b".to_string(), "online", "free")
6110 ]
6111 );
6112 assert_eq!(
6113 contacts(&client_d, cx_d),
6114 [("user_b".to_string(), "online", "free")]
6115 );
6116
6117 active_call_a
6118 .update(cx_a, |call, cx| {
6119 call.invite(client_b.user_id().unwrap(), None, cx)
6120 })
6121 .await
6122 .unwrap();
6123 executor.run_until_parked();
6124 assert_eq!(
6125 contacts(&client_a, cx_a),
6126 [
6127 ("user_b".to_string(), "online", "busy"),
6128 ("user_c".to_string(), "online", "free")
6129 ]
6130 );
6131 assert_eq!(
6132 contacts(&client_b, cx_b),
6133 [
6134 ("user_a".to_string(), "online", "busy"),
6135 ("user_c".to_string(), "online", "free"),
6136 ("user_d".to_string(), "online", "free")
6137 ]
6138 );
6139 assert_eq!(
6140 contacts(&client_c, cx_c),
6141 [
6142 ("user_a".to_string(), "online", "busy"),
6143 ("user_b".to_string(), "online", "busy")
6144 ]
6145 );
6146 assert_eq!(
6147 contacts(&client_d, cx_d),
6148 [("user_b".to_string(), "online", "busy")]
6149 );
6150
6151 server.forbid_connections();
6152 server.disconnect_client(client_a.peer_id().unwrap());
6153 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
6154 assert_eq!(contacts(&client_a, cx_a), []);
6155 assert_eq!(
6156 contacts(&client_b, cx_b),
6157 [
6158 ("user_a".to_string(), "offline", "free"),
6159 ("user_c".to_string(), "online", "free"),
6160 ("user_d".to_string(), "online", "free")
6161 ]
6162 );
6163 assert_eq!(
6164 contacts(&client_c, cx_c),
6165 [
6166 ("user_a".to_string(), "offline", "free"),
6167 ("user_b".to_string(), "online", "free")
6168 ]
6169 );
6170 assert_eq!(
6171 contacts(&client_d, cx_d),
6172 [("user_b".to_string(), "online", "free")]
6173 );
6174
6175 // Test removing a contact
6176 client_b
6177 .user_store()
6178 .update(cx_b, |store, cx| {
6179 store.remove_contact(client_c.user_id().unwrap(), cx)
6180 })
6181 .await
6182 .unwrap();
6183 executor.run_until_parked();
6184 assert_eq!(
6185 contacts(&client_b, cx_b),
6186 [
6187 ("user_a".to_string(), "offline", "free"),
6188 ("user_d".to_string(), "online", "free")
6189 ]
6190 );
6191 assert_eq!(
6192 contacts(&client_c, cx_c),
6193 [("user_a".to_string(), "offline", "free"),]
6194 );
6195
6196 fn contacts(
6197 client: &TestClient,
6198 cx: &TestAppContext,
6199 ) -> Vec<(String, &'static str, &'static str)> {
6200 client.user_store().read_with(cx, |store, _| {
6201 store
6202 .contacts()
6203 .iter()
6204 .map(|contact| {
6205 (
6206 contact.user.github_login.clone().to_string(),
6207 if contact.online { "online" } else { "offline" },
6208 if contact.busy { "busy" } else { "free" },
6209 )
6210 })
6211 .collect()
6212 })
6213 }
6214}
6215
6216#[gpui::test(iterations = 10)]
6217async fn test_contact_requests(
6218 executor: BackgroundExecutor,
6219 cx_a: &mut TestAppContext,
6220 cx_a2: &mut TestAppContext,
6221 cx_b: &mut TestAppContext,
6222 cx_b2: &mut TestAppContext,
6223 cx_c: &mut TestAppContext,
6224 cx_c2: &mut TestAppContext,
6225) {
6226 // Connect to a server as 3 clients.
6227 let mut server = TestServer::start(executor.clone()).await;
6228 let client_a = server.create_client(cx_a, "user_a").await;
6229 let client_a2 = server.create_client(cx_a2, "user_a").await;
6230 let client_b = server.create_client(cx_b, "user_b").await;
6231 let client_b2 = server.create_client(cx_b2, "user_b").await;
6232 let client_c = server.create_client(cx_c, "user_c").await;
6233 let client_c2 = server.create_client(cx_c2, "user_c").await;
6234
6235 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
6236 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
6237 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
6238
6239 // User A and User C request that user B become their contact.
6240 client_a
6241 .user_store()
6242 .update(cx_a, |store, cx| {
6243 store.request_contact(client_b.user_id().unwrap(), cx)
6244 })
6245 .await
6246 .unwrap();
6247 client_c
6248 .user_store()
6249 .update(cx_c, |store, cx| {
6250 store.request_contact(client_b.user_id().unwrap(), cx)
6251 })
6252 .await
6253 .unwrap();
6254 executor.run_until_parked();
6255
6256 // All users see the pending request appear in all their clients.
6257 assert_eq!(
6258 client_a.summarize_contacts(cx_a).outgoing_requests,
6259 &["user_b"]
6260 );
6261 assert_eq!(
6262 client_a2.summarize_contacts(cx_a2).outgoing_requests,
6263 &["user_b"]
6264 );
6265 assert_eq!(
6266 client_b.summarize_contacts(cx_b).incoming_requests,
6267 &["user_a", "user_c"]
6268 );
6269 assert_eq!(
6270 client_b2.summarize_contacts(cx_b2).incoming_requests,
6271 &["user_a", "user_c"]
6272 );
6273 assert_eq!(
6274 client_c.summarize_contacts(cx_c).outgoing_requests,
6275 &["user_b"]
6276 );
6277 assert_eq!(
6278 client_c2.summarize_contacts(cx_c2).outgoing_requests,
6279 &["user_b"]
6280 );
6281
6282 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
6283 disconnect_and_reconnect(&client_a, cx_a).await;
6284 disconnect_and_reconnect(&client_b, cx_b).await;
6285 disconnect_and_reconnect(&client_c, cx_c).await;
6286 executor.run_until_parked();
6287 assert_eq!(
6288 client_a.summarize_contacts(cx_a).outgoing_requests,
6289 &["user_b"]
6290 );
6291 assert_eq!(
6292 client_b.summarize_contacts(cx_b).incoming_requests,
6293 &["user_a", "user_c"]
6294 );
6295 assert_eq!(
6296 client_c.summarize_contacts(cx_c).outgoing_requests,
6297 &["user_b"]
6298 );
6299
6300 // User B accepts the request from user A.
6301 client_b
6302 .user_store()
6303 .update(cx_b, |store, cx| {
6304 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
6305 })
6306 .await
6307 .unwrap();
6308
6309 executor.run_until_parked();
6310
6311 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
6312 let contacts_b = client_b.summarize_contacts(cx_b);
6313 assert_eq!(contacts_b.current, &["user_a"]);
6314 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
6315 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6316 assert_eq!(contacts_b2.current, &["user_a"]);
6317 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
6318
6319 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
6320 let contacts_a = client_a.summarize_contacts(cx_a);
6321 assert_eq!(contacts_a.current, &["user_b"]);
6322 assert!(contacts_a.outgoing_requests.is_empty());
6323 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
6324 assert_eq!(contacts_a2.current, &["user_b"]);
6325 assert!(contacts_a2.outgoing_requests.is_empty());
6326
6327 // Contacts are present upon connecting (tested here via disconnect/reconnect)
6328 disconnect_and_reconnect(&client_a, cx_a).await;
6329 disconnect_and_reconnect(&client_b, cx_b).await;
6330 disconnect_and_reconnect(&client_c, cx_c).await;
6331 executor.run_until_parked();
6332 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6333 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6334 assert_eq!(
6335 client_b.summarize_contacts(cx_b).incoming_requests,
6336 &["user_c"]
6337 );
6338 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6339 assert_eq!(
6340 client_c.summarize_contacts(cx_c).outgoing_requests,
6341 &["user_b"]
6342 );
6343
6344 // User B rejects the request from user C.
6345 client_b
6346 .user_store()
6347 .update(cx_b, |store, cx| {
6348 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
6349 })
6350 .await
6351 .unwrap();
6352
6353 executor.run_until_parked();
6354
6355 // User B doesn't see user C as their contact, and the incoming request from them is removed.
6356 let contacts_b = client_b.summarize_contacts(cx_b);
6357 assert_eq!(contacts_b.current, &["user_a"]);
6358 assert!(contacts_b.incoming_requests.is_empty());
6359 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6360 assert_eq!(contacts_b2.current, &["user_a"]);
6361 assert!(contacts_b2.incoming_requests.is_empty());
6362
6363 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
6364 let contacts_c = client_c.summarize_contacts(cx_c);
6365 assert!(contacts_c.current.is_empty());
6366 assert!(contacts_c.outgoing_requests.is_empty());
6367 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
6368 assert!(contacts_c2.current.is_empty());
6369 assert!(contacts_c2.outgoing_requests.is_empty());
6370
6371 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
6372 disconnect_and_reconnect(&client_a, cx_a).await;
6373 disconnect_and_reconnect(&client_b, cx_b).await;
6374 disconnect_and_reconnect(&client_c, cx_c).await;
6375 executor.run_until_parked();
6376 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6377 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6378 assert!(
6379 client_b
6380 .summarize_contacts(cx_b)
6381 .incoming_requests
6382 .is_empty()
6383 );
6384 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6385 assert!(
6386 client_c
6387 .summarize_contacts(cx_c)
6388 .outgoing_requests
6389 .is_empty()
6390 );
6391
6392 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
6393 client.disconnect(&cx.to_async());
6394 client.clear_contacts(cx).await;
6395 client
6396 .connect(false, &cx.to_async())
6397 .await
6398 .into_response()
6399 .unwrap();
6400 }
6401}
6402
6403#[gpui::test(iterations = 10)]
6404async fn test_join_call_after_screen_was_shared(
6405 executor: BackgroundExecutor,
6406 cx_a: &mut TestAppContext,
6407 cx_b: &mut TestAppContext,
6408) {
6409 let mut server = TestServer::start(executor.clone()).await;
6410
6411 let client_a = server.create_client(cx_a, "user_a").await;
6412 let client_b = server.create_client(cx_b, "user_b").await;
6413 server
6414 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6415 .await;
6416
6417 let active_call_a = cx_a.read(ActiveCall::global);
6418 let active_call_b = cx_b.read(ActiveCall::global);
6419
6420 // Call users B and C from client A.
6421 active_call_a
6422 .update(cx_a, |call, cx| {
6423 call.invite(client_b.user_id().unwrap(), None, cx)
6424 })
6425 .await
6426 .unwrap();
6427
6428 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
6429 executor.run_until_parked();
6430 assert_eq!(
6431 room_participants(&room_a, cx_a),
6432 RoomParticipants {
6433 remote: Default::default(),
6434 pending: vec!["user_b".to_string()]
6435 }
6436 );
6437
6438 // User B receives the call.
6439
6440 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
6441 let call_b = incoming_call_b.next().await.unwrap().unwrap();
6442 assert_eq!(call_b.calling_user.github_login, "user_a");
6443
6444 // User A shares their screen
6445 let display = gpui::TestScreenCaptureSource::new();
6446 cx_a.set_screen_capture_sources(vec![display]);
6447 let screen_a = cx_a
6448 .update(|cx| cx.screen_capture_sources())
6449 .await
6450 .unwrap()
6451 .unwrap()
6452 .into_iter()
6453 .next()
6454 .unwrap();
6455
6456 active_call_a
6457 .update(cx_a, |call, cx| {
6458 call.room()
6459 .unwrap()
6460 .update(cx, |room, cx| room.share_screen(screen_a, cx))
6461 })
6462 .await
6463 .unwrap();
6464
6465 client_b.user_store().update(cx_b, |user_store, _| {
6466 user_store.clear_cache();
6467 });
6468
6469 // User B joins the room
6470 active_call_b
6471 .update(cx_b, |call, cx| call.accept_incoming(cx))
6472 .await
6473 .unwrap();
6474
6475 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
6476 assert!(incoming_call_b.next().await.unwrap().is_none());
6477
6478 executor.run_until_parked();
6479 assert_eq!(
6480 room_participants(&room_a, cx_a),
6481 RoomParticipants {
6482 remote: vec!["user_b".to_string()],
6483 pending: vec![],
6484 }
6485 );
6486 assert_eq!(
6487 room_participants(&room_b, cx_b),
6488 RoomParticipants {
6489 remote: vec!["user_a".to_string()],
6490 pending: vec![],
6491 }
6492 );
6493
6494 // Ensure User B sees User A's screenshare.
6495
6496 room_b.read_with(cx_b, |room, _| {
6497 assert_eq!(
6498 room.remote_participants()
6499 .get(&client_a.user_id().unwrap())
6500 .unwrap()
6501 .video_tracks
6502 .len(),
6503 1
6504 );
6505 });
6506}
6507
6508#[gpui::test]
6509async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
6510 let mut server = TestServer::start(cx.executor().clone()).await;
6511 let client_a = server.create_client(cx, "user_a").await;
6512 let (_workspace_a, cx) = client_a.build_test_workspace(cx).await;
6513
6514 cx.simulate_resize(size(px(300.), px(300.)));
6515
6516 cx.simulate_keystrokes("cmd-n cmd-n cmd-n");
6517 cx.update(|window, _cx| window.refresh());
6518
6519 let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap();
6520
6521 cx.simulate_event(MouseDownEvent {
6522 button: MouseButton::Right,
6523 position: new_tab_button_bounds.center(),
6524 modifiers: Modifiers::default(),
6525 click_count: 1,
6526 first_mouse: false,
6527 });
6528
6529 // regression test that the right click menu for tabs does not open.
6530 assert!(cx.debug_bounds("MENU_ITEM-Close").is_none());
6531
6532 let tab_bounds = cx.debug_bounds("TAB-1").unwrap();
6533 cx.simulate_event(MouseDownEvent {
6534 button: MouseButton::Right,
6535 position: tab_bounds.center(),
6536 modifiers: Modifiers::default(),
6537 click_count: 1,
6538 first_mouse: false,
6539 });
6540 assert!(cx.debug_bounds("MENU_ITEM-Close").is_some());
6541}
6542
6543#[gpui::test]
6544async fn test_pane_split_left(cx: &mut TestAppContext) {
6545 let (_, client) = TestServer::start1(cx).await;
6546 let (workspace, cx) = client.build_test_workspace(cx).await;
6547
6548 cx.simulate_keystrokes("cmd-n");
6549 workspace.update(cx, |workspace, cx| {
6550 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 1);
6551 });
6552 cx.simulate_keystrokes("cmd-k left");
6553 workspace.update(cx, |workspace, cx| {
6554 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
6555 });
6556 cx.simulate_keystrokes("cmd-k");
6557 // Sleep past the historical timeout to ensure the multi-stroke binding
6558 // still fires now that unambiguous prefixes no longer auto-expire.
6559 cx.executor().advance_clock(Duration::from_secs(2));
6560 cx.simulate_keystrokes("left");
6561 workspace.update(cx, |workspace, cx| {
6562 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 3);
6563 });
6564}
6565
6566#[gpui::test]
6567async fn test_join_after_restart(cx1: &mut TestAppContext, cx2: &mut TestAppContext) {
6568 let (mut server, client) = TestServer::start1(cx1).await;
6569 let channel1 = server.make_public_channel("channel1", &client, cx1).await;
6570 let channel2 = server.make_public_channel("channel2", &client, cx1).await;
6571
6572 join_channel(channel1, &client, cx1).await.unwrap();
6573 drop(client);
6574
6575 let client2 = server.create_client(cx2, "user_a").await;
6576 join_channel(channel2, &client2, cx2).await.unwrap();
6577}
6578
6579#[gpui::test]
6580async fn test_preview_tabs(cx: &mut TestAppContext) {
6581 let (_server, client) = TestServer::start1(cx).await;
6582 let (workspace, cx) = client.build_test_workspace(cx).await;
6583 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone());
6584
6585 let worktree_id = project.update(cx, |project, cx| {
6586 project.worktrees(cx).next().unwrap().read(cx).id()
6587 });
6588
6589 let path_1 = ProjectPath {
6590 worktree_id,
6591 path: rel_path("1.txt").into(),
6592 };
6593 let path_2 = ProjectPath {
6594 worktree_id,
6595 path: rel_path("2.js").into(),
6596 };
6597 let path_3 = ProjectPath {
6598 worktree_id,
6599 path: rel_path("3.rs").into(),
6600 };
6601
6602 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6603
6604 let get_path = |pane: &Pane, idx: usize, cx: &App| {
6605 pane.item_for_index(idx).unwrap().project_path(cx).unwrap()
6606 };
6607
6608 // Opening item 3 as a "permanent" tab
6609 workspace
6610 .update_in(cx, |workspace, window, cx| {
6611 workspace.open_path(path_3.clone(), None, false, window, cx)
6612 })
6613 .await
6614 .unwrap();
6615
6616 pane.update(cx, |pane, cx| {
6617 assert_eq!(pane.items_len(), 1);
6618 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6619 assert_eq!(pane.preview_item_id(), None);
6620
6621 assert!(!pane.can_navigate_backward());
6622 assert!(!pane.can_navigate_forward());
6623 });
6624
6625 // Open item 1 as preview
6626 workspace
6627 .update_in(cx, |workspace, window, cx| {
6628 workspace.open_path_preview(path_1.clone(), None, true, true, true, window, cx)
6629 })
6630 .await
6631 .unwrap();
6632
6633 pane.update(cx, |pane, cx| {
6634 assert_eq!(pane.items_len(), 2);
6635 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6636 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6637 assert_eq!(
6638 pane.preview_item_id(),
6639 Some(pane.items().nth(1).unwrap().item_id())
6640 );
6641
6642 assert!(pane.can_navigate_backward());
6643 assert!(!pane.can_navigate_forward());
6644 });
6645
6646 // Open item 2 as preview
6647 workspace
6648 .update_in(cx, |workspace, window, cx| {
6649 workspace.open_path_preview(path_2.clone(), None, true, true, true, window, cx)
6650 })
6651 .await
6652 .unwrap();
6653
6654 pane.update(cx, |pane, cx| {
6655 assert_eq!(pane.items_len(), 2);
6656 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6657 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6658 assert_eq!(
6659 pane.preview_item_id(),
6660 Some(pane.items().nth(1).unwrap().item_id())
6661 );
6662
6663 assert!(pane.can_navigate_backward());
6664 assert!(!pane.can_navigate_forward());
6665 });
6666
6667 // Going back should show item 1 as preview
6668 workspace
6669 .update_in(cx, |workspace, window, cx| {
6670 workspace.go_back(pane.downgrade(), window, cx)
6671 })
6672 .await
6673 .unwrap();
6674
6675 pane.update(cx, |pane, cx| {
6676 assert_eq!(pane.items_len(), 2);
6677 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6678 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6679 assert_eq!(
6680 pane.preview_item_id(),
6681 Some(pane.items().nth(1).unwrap().item_id())
6682 );
6683
6684 assert!(pane.can_navigate_backward());
6685 assert!(pane.can_navigate_forward());
6686 });
6687
6688 // Closing item 1
6689 pane.update_in(cx, |pane, window, cx| {
6690 pane.close_item_by_id(
6691 pane.active_item().unwrap().item_id(),
6692 workspace::SaveIntent::Skip,
6693 window,
6694 cx,
6695 )
6696 })
6697 .await
6698 .unwrap();
6699
6700 pane.update(cx, |pane, cx| {
6701 assert_eq!(pane.items_len(), 1);
6702 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6703 assert_eq!(pane.preview_item_id(), None);
6704
6705 assert!(pane.can_navigate_backward());
6706 assert!(!pane.can_navigate_forward());
6707 });
6708
6709 // Going back should show item 1 as preview
6710 workspace
6711 .update_in(cx, |workspace, window, cx| {
6712 workspace.go_back(pane.downgrade(), window, cx)
6713 })
6714 .await
6715 .unwrap();
6716
6717 pane.update(cx, |pane, cx| {
6718 assert_eq!(pane.items_len(), 2);
6719 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6720 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6721 assert_eq!(
6722 pane.preview_item_id(),
6723 Some(pane.items().nth(1).unwrap().item_id())
6724 );
6725
6726 assert!(pane.can_navigate_backward());
6727 assert!(pane.can_navigate_forward());
6728 });
6729
6730 // Close permanent tab
6731 pane.update_in(cx, |pane, window, cx| {
6732 let id = pane.items().next().unwrap().item_id();
6733 pane.close_item_by_id(id, workspace::SaveIntent::Skip, window, cx)
6734 })
6735 .await
6736 .unwrap();
6737
6738 pane.update(cx, |pane, cx| {
6739 assert_eq!(pane.items_len(), 1);
6740 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6741 assert_eq!(
6742 pane.preview_item_id(),
6743 Some(pane.items().next().unwrap().item_id())
6744 );
6745
6746 assert!(pane.can_navigate_backward());
6747 assert!(pane.can_navigate_forward());
6748 });
6749
6750 // Split pane to the right
6751 pane.update_in(cx, |pane, window, cx| {
6752 pane.split(
6753 workspace::SplitDirection::Right,
6754 workspace::SplitMode::default(),
6755 window,
6756 cx,
6757 );
6758 });
6759 cx.run_until_parked();
6760 let right_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
6761
6762 pane.update(cx, |pane, cx| {
6763 assert_eq!(pane.items_len(), 1);
6764 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6765 assert_eq!(
6766 pane.preview_item_id(),
6767 Some(pane.items().next().unwrap().item_id())
6768 );
6769
6770 assert!(pane.can_navigate_backward());
6771 assert!(pane.can_navigate_forward());
6772 });
6773
6774 right_pane.update(cx, |pane, cx| {
6775 assert_eq!(pane.items_len(), 1);
6776 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6777 assert_eq!(pane.preview_item_id(), None);
6778
6779 assert!(!pane.can_navigate_backward());
6780 assert!(!pane.can_navigate_forward());
6781 });
6782
6783 // Open item 2 as preview in right pane
6784 workspace
6785 .update_in(cx, |workspace, window, cx| {
6786 workspace.open_path_preview(path_2.clone(), None, true, true, true, window, cx)
6787 })
6788 .await
6789 .unwrap();
6790
6791 pane.update(cx, |pane, cx| {
6792 assert_eq!(pane.items_len(), 1);
6793 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6794 assert_eq!(
6795 pane.preview_item_id(),
6796 Some(pane.items().next().unwrap().item_id())
6797 );
6798
6799 assert!(pane.can_navigate_backward());
6800 assert!(pane.can_navigate_forward());
6801 });
6802
6803 right_pane.update(cx, |pane, cx| {
6804 assert_eq!(pane.items_len(), 2);
6805 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6806 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6807 assert_eq!(
6808 pane.preview_item_id(),
6809 Some(pane.items().nth(1).unwrap().item_id())
6810 );
6811
6812 assert!(pane.can_navigate_backward());
6813 assert!(!pane.can_navigate_forward());
6814 });
6815
6816 // Focus left pane
6817 workspace.update_in(cx, |workspace, window, cx| {
6818 workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx)
6819 });
6820
6821 // Open item 2 as preview in left pane
6822 workspace
6823 .update_in(cx, |workspace, window, cx| {
6824 workspace.open_path_preview(path_2.clone(), None, true, true, true, window, cx)
6825 })
6826 .await
6827 .unwrap();
6828
6829 pane.update(cx, |pane, cx| {
6830 assert_eq!(pane.items_len(), 1);
6831 assert_eq!(get_path(pane, 0, cx), path_2.clone());
6832 assert_eq!(
6833 pane.preview_item_id(),
6834 Some(pane.items().next().unwrap().item_id())
6835 );
6836
6837 assert!(pane.can_navigate_backward());
6838 assert!(!pane.can_navigate_forward());
6839 });
6840
6841 right_pane.update(cx, |pane, cx| {
6842 assert_eq!(pane.items_len(), 2);
6843 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6844 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6845 assert_eq!(
6846 pane.preview_item_id(),
6847 Some(pane.items().nth(1).unwrap().item_id())
6848 );
6849
6850 assert!(pane.can_navigate_backward());
6851 assert!(!pane.can_navigate_forward());
6852 });
6853}
6854
6855#[gpui::test(iterations = 10)]
6856async fn test_context_collaboration_with_reconnect(
6857 executor: BackgroundExecutor,
6858 cx_a: &mut TestAppContext,
6859 cx_b: &mut TestAppContext,
6860) {
6861 let mut server = TestServer::start(executor.clone()).await;
6862 let client_a = server.create_client(cx_a, "user_a").await;
6863 let client_b = server.create_client(cx_b, "user_b").await;
6864 server
6865 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6866 .await;
6867 let active_call_a = cx_a.read(ActiveCall::global);
6868
6869 client_a.fs().insert_tree("/a", Default::default()).await;
6870 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
6871 let project_id = active_call_a
6872 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6873 .await
6874 .unwrap();
6875 let project_b = client_b.join_remote_project(project_id, cx_b).await;
6876
6877 // Client A sees that a guest has joined.
6878 executor.run_until_parked();
6879
6880 project_a.read_with(cx_a, |project, _| {
6881 assert_eq!(project.collaborators().len(), 1);
6882 });
6883 project_b.read_with(cx_b, |project, _| {
6884 assert_eq!(project.collaborators().len(), 1);
6885 });
6886
6887 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
6888 let text_thread_store_a = cx_a
6889 .update(|cx| {
6890 TextThreadStore::new(
6891 project_a.clone(),
6892 prompt_builder.clone(),
6893 Arc::new(SlashCommandWorkingSet::default()),
6894 cx,
6895 )
6896 })
6897 .await
6898 .unwrap();
6899 let text_thread_store_b = cx_b
6900 .update(|cx| {
6901 TextThreadStore::new(
6902 project_b.clone(),
6903 prompt_builder.clone(),
6904 Arc::new(SlashCommandWorkingSet::default()),
6905 cx,
6906 )
6907 })
6908 .await
6909 .unwrap();
6910
6911 // Client A creates a new chats.
6912 let text_thread_a = text_thread_store_a.update(cx_a, |store, cx| store.create(cx));
6913 executor.run_until_parked();
6914
6915 // Client B retrieves host's contexts and joins one.
6916 let text_thread_b = text_thread_store_b
6917 .update(cx_b, |store, cx| {
6918 let host_text_threads = store.host_text_threads().collect::<Vec<_>>();
6919 assert_eq!(host_text_threads.len(), 1);
6920 store.open_remote(host_text_threads[0].id.clone(), cx)
6921 })
6922 .await
6923 .unwrap();
6924
6925 // Host and guest make changes
6926 text_thread_a.update(cx_a, |text_thread, cx| {
6927 text_thread.buffer().update(cx, |buffer, cx| {
6928 buffer.edit([(0..0, "Host change\n")], None, cx)
6929 })
6930 });
6931 text_thread_b.update(cx_b, |text_thread, cx| {
6932 text_thread.buffer().update(cx, |buffer, cx| {
6933 buffer.edit([(0..0, "Guest change\n")], None, cx)
6934 })
6935 });
6936 executor.run_until_parked();
6937 assert_eq!(
6938 text_thread_a.read_with(cx_a, |text_thread, cx| text_thread.buffer().read(cx).text()),
6939 "Guest change\nHost change\n"
6940 );
6941 assert_eq!(
6942 text_thread_b.read_with(cx_b, |text_thread, cx| text_thread.buffer().read(cx).text()),
6943 "Guest change\nHost change\n"
6944 );
6945
6946 // Disconnect client A and make some changes while disconnected.
6947 server.disconnect_client(client_a.peer_id().unwrap());
6948 server.forbid_connections();
6949 text_thread_a.update(cx_a, |text_thread, cx| {
6950 text_thread.buffer().update(cx, |buffer, cx| {
6951 buffer.edit([(0..0, "Host offline change\n")], None, cx)
6952 })
6953 });
6954 text_thread_b.update(cx_b, |text_thread, cx| {
6955 text_thread.buffer().update(cx, |buffer, cx| {
6956 buffer.edit([(0..0, "Guest offline change\n")], None, cx)
6957 })
6958 });
6959 executor.run_until_parked();
6960 assert_eq!(
6961 text_thread_a.read_with(cx_a, |text_thread, cx| text_thread.buffer().read(cx).text()),
6962 "Host offline change\nGuest change\nHost change\n"
6963 );
6964 assert_eq!(
6965 text_thread_b.read_with(cx_b, |text_thread, cx| text_thread.buffer().read(cx).text()),
6966 "Guest offline change\nGuest change\nHost change\n"
6967 );
6968
6969 // Allow client A to reconnect and verify that contexts converge.
6970 server.allow_connections();
6971 executor.advance_clock(RECEIVE_TIMEOUT);
6972 assert_eq!(
6973 text_thread_a.read_with(cx_a, |text_thread, cx| text_thread.buffer().read(cx).text()),
6974 "Guest offline change\nHost offline change\nGuest change\nHost change\n"
6975 );
6976 assert_eq!(
6977 text_thread_b.read_with(cx_b, |text_thread, cx| text_thread.buffer().read(cx).text()),
6978 "Guest offline change\nHost offline change\nGuest change\nHost change\n"
6979 );
6980
6981 // Client A disconnects without being able to reconnect. Context B becomes readonly.
6982 server.forbid_connections();
6983 server.disconnect_client(client_a.peer_id().unwrap());
6984 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
6985 text_thread_b.read_with(cx_b, |text_thread, cx| {
6986 assert!(text_thread.buffer().read(cx).read_only());
6987 });
6988}
6989
6990#[gpui::test]
6991async fn test_remote_git_branches(
6992 executor: BackgroundExecutor,
6993 cx_a: &mut TestAppContext,
6994 cx_b: &mut TestAppContext,
6995) {
6996 let mut server = TestServer::start(executor.clone()).await;
6997 let client_a = server.create_client(cx_a, "user_a").await;
6998 let client_b = server.create_client(cx_b, "user_b").await;
6999 server
7000 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7001 .await;
7002 let active_call_a = cx_a.read(ActiveCall::global);
7003
7004 client_a
7005 .fs()
7006 .insert_tree("/project", serde_json::json!({ ".git":{} }))
7007 .await;
7008 let branches = ["main", "dev", "feature-1"];
7009 client_a
7010 .fs()
7011 .insert_branches(Path::new("/project/.git"), &branches);
7012 let branches_set = branches
7013 .into_iter()
7014 .map(ToString::to_string)
7015 .collect::<HashSet<_>>();
7016
7017 let (project_a, _) = client_a.build_local_project("/project", cx_a).await;
7018
7019 let project_id = active_call_a
7020 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7021 .await
7022 .unwrap();
7023 let project_b = client_b.join_remote_project(project_id, cx_b).await;
7024
7025 // Client A sees that a guest has joined and the repo has been populated
7026 executor.run_until_parked();
7027
7028 let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
7029
7030 let branches_b = cx_b
7031 .update(|cx| repo_b.update(cx, |repository, _| repository.branches()))
7032 .await
7033 .unwrap()
7034 .unwrap();
7035
7036 let new_branch = branches[2];
7037
7038 let branches_b = branches_b
7039 .into_iter()
7040 .map(|branch| branch.name().to_string())
7041 .collect::<HashSet<_>>();
7042
7043 assert_eq!(branches_b, branches_set);
7044
7045 cx_b.update(|cx| {
7046 repo_b.update(cx, |repository, _cx| {
7047 repository.change_branch(new_branch.to_string())
7048 })
7049 })
7050 .await
7051 .unwrap()
7052 .unwrap();
7053
7054 executor.run_until_parked();
7055
7056 let host_branch = cx_a.update(|cx| {
7057 project_a.update(cx, |project, cx| {
7058 project
7059 .repositories(cx)
7060 .values()
7061 .next()
7062 .unwrap()
7063 .read(cx)
7064 .branch
7065 .as_ref()
7066 .unwrap()
7067 .clone()
7068 })
7069 });
7070
7071 assert_eq!(host_branch.name(), branches[2]);
7072
7073 // Also try creating a new branch
7074 cx_b.update(|cx| {
7075 repo_b.update(cx, |repository, _cx| {
7076 repository.create_branch("totally-new-branch".to_string(), None)
7077 })
7078 })
7079 .await
7080 .unwrap()
7081 .unwrap();
7082
7083 cx_b.update(|cx| {
7084 repo_b.update(cx, |repository, _cx| {
7085 repository.change_branch("totally-new-branch".to_string())
7086 })
7087 })
7088 .await
7089 .unwrap()
7090 .unwrap();
7091
7092 executor.run_until_parked();
7093
7094 let host_branch = cx_a.update(|cx| {
7095 project_a.update(cx, |project, cx| {
7096 project
7097 .repositories(cx)
7098 .values()
7099 .next()
7100 .unwrap()
7101 .read(cx)
7102 .branch
7103 .as_ref()
7104 .unwrap()
7105 .clone()
7106 })
7107 });
7108
7109 assert_eq!(host_branch.name(), "totally-new-branch");
7110}