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