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