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