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