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