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