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