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