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