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