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