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