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::{shared_screen::SharedScreen, 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 active_call_a = cx_a.read(ActiveCall::global);
1078 let active_call_b = cx_b.read(ActiveCall::global);
1079
1080 let a_notified = Rc::new(Cell::new(false));
1081 cx_a.update({
1082 let notified = a_notified.clone();
1083 |cx| {
1084 cx.observe(&active_call_a, move |_, _| notified.set(true))
1085 .detach()
1086 }
1087 });
1088
1089 let b_notified = Rc::new(Cell::new(false));
1090 cx_b.update({
1091 let b_notified = b_notified.clone();
1092 |cx| {
1093 cx.observe(&active_call_b, move |_, _| b_notified.set(true))
1094 .detach()
1095 }
1096 });
1097
1098 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
1099 active_call_a
1100 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1101 .await
1102 .unwrap();
1103 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
1104
1105 server
1106 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1107 .await;
1108 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
1109 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
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 active_call_b
1165 .update(cx_b, |call, cx| call.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 active_call_b
1191 .update(cx_b, |call, cx| call.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(
5062 deterministic: Arc<Deterministic>,
5063 cx_a: &mut TestAppContext,
5064 cx_b: &mut TestAppContext,
5065) {
5066 cx_a.foreground().forbid_parking();
5067 cx_a.update(editor::init);
5068 cx_b.update(editor::init);
5069
5070 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
5071 let client_a = server.create_client(cx_a, "user_a").await;
5072 let client_b = server.create_client(cx_b, "user_b").await;
5073 server
5074 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5075 .await;
5076 let active_call_a = cx_a.read(ActiveCall::global);
5077 let active_call_b = cx_b.read(ActiveCall::global);
5078
5079 client_a
5080 .fs
5081 .insert_tree(
5082 "/a",
5083 json!({
5084 "1.txt": "one",
5085 "2.txt": "two",
5086 "3.txt": "three",
5087 }),
5088 )
5089 .await;
5090 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
5091 active_call_a
5092 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
5093 .await
5094 .unwrap();
5095
5096 let project_id = active_call_a
5097 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5098 .await
5099 .unwrap();
5100 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5101 active_call_b
5102 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
5103 .await
5104 .unwrap();
5105
5106 // Client A opens some editors.
5107 let workspace_a = client_a.build_workspace(&project_a, cx_a);
5108 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
5109 let editor_a1 = workspace_a
5110 .update(cx_a, |workspace, cx| {
5111 workspace.open_path((worktree_id, "1.txt"), true, cx)
5112 })
5113 .await
5114 .unwrap()
5115 .downcast::<Editor>()
5116 .unwrap();
5117 let editor_a2 = workspace_a
5118 .update(cx_a, |workspace, cx| {
5119 workspace.open_path((worktree_id, "2.txt"), true, cx)
5120 })
5121 .await
5122 .unwrap()
5123 .downcast::<Editor>()
5124 .unwrap();
5125
5126 // Client B opens an editor.
5127 let workspace_b = client_b.build_workspace(&project_b, cx_b);
5128 let editor_b1 = workspace_b
5129 .update(cx_b, |workspace, cx| {
5130 workspace.open_path((worktree_id, "1.txt"), true, cx)
5131 })
5132 .await
5133 .unwrap()
5134 .downcast::<Editor>()
5135 .unwrap();
5136
5137 let client_a_id = project_b.read_with(cx_b, |project, _| {
5138 project.collaborators().values().next().unwrap().peer_id
5139 });
5140 let client_b_id = project_a.read_with(cx_a, |project, _| {
5141 project.collaborators().values().next().unwrap().peer_id
5142 });
5143
5144 // When client B starts following client A, all visible view states are replicated to client B.
5145 editor_a1.update(cx_a, |editor, cx| {
5146 editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
5147 });
5148 editor_a2.update(cx_a, |editor, cx| {
5149 editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
5150 });
5151 workspace_b
5152 .update(cx_b, |workspace, cx| {
5153 workspace
5154 .toggle_follow(&ToggleFollow(client_a_id), cx)
5155 .unwrap()
5156 })
5157 .await
5158 .unwrap();
5159
5160 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
5161 workspace
5162 .active_item(cx)
5163 .unwrap()
5164 .downcast::<Editor>()
5165 .unwrap()
5166 });
5167 assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
5168 assert_eq!(
5169 editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)),
5170 Some((worktree_id, "2.txt").into())
5171 );
5172 assert_eq!(
5173 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
5174 vec![2..3]
5175 );
5176 assert_eq!(
5177 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
5178 vec![0..1]
5179 );
5180
5181 // When client A activates a different editor, client B does so as well.
5182 workspace_a.update(cx_a, |workspace, cx| {
5183 workspace.activate_item(&editor_a1, cx)
5184 });
5185 workspace_b
5186 .condition(cx_b, |workspace, cx| {
5187 workspace.active_item(cx).unwrap().id() == editor_b1.id()
5188 })
5189 .await;
5190
5191 // When client A navigates back and forth, client B does so as well.
5192 workspace_a
5193 .update(cx_a, |workspace, cx| {
5194 workspace::Pane::go_back(workspace, None, cx)
5195 })
5196 .await;
5197 workspace_b
5198 .condition(cx_b, |workspace, cx| {
5199 workspace.active_item(cx).unwrap().id() == editor_b2.id()
5200 })
5201 .await;
5202
5203 workspace_a
5204 .update(cx_a, |workspace, cx| {
5205 workspace::Pane::go_forward(workspace, None, cx)
5206 })
5207 .await;
5208 workspace_b
5209 .condition(cx_b, |workspace, cx| {
5210 workspace.active_item(cx).unwrap().id() == editor_b1.id()
5211 })
5212 .await;
5213
5214 // Changes to client A's editor are reflected on client B.
5215 editor_a1.update(cx_a, |editor, cx| {
5216 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
5217 });
5218 editor_b1
5219 .condition(cx_b, |editor, cx| {
5220 editor.selections.ranges(cx) == vec![1..1, 2..2]
5221 })
5222 .await;
5223
5224 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
5225 editor_b1
5226 .condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
5227 .await;
5228
5229 editor_a1.update(cx_a, |editor, cx| {
5230 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
5231 editor.set_scroll_position(vec2f(0., 100.), cx);
5232 });
5233 editor_b1
5234 .condition(cx_b, |editor, cx| {
5235 editor.selections.ranges(cx) == vec![3..3]
5236 })
5237 .await;
5238
5239 // After unfollowing, client B stops receiving updates from client A.
5240 workspace_b.update(cx_b, |workspace, cx| {
5241 workspace.unfollow(&workspace.active_pane().clone(), cx)
5242 });
5243 workspace_a.update(cx_a, |workspace, cx| {
5244 workspace.activate_item(&editor_a2, cx)
5245 });
5246 deterministic.run_until_parked();
5247 assert_eq!(
5248 workspace_b.read_with(cx_b, |workspace, cx| workspace
5249 .active_item(cx)
5250 .unwrap()
5251 .id()),
5252 editor_b1.id()
5253 );
5254
5255 // Client A starts following client B.
5256 workspace_a
5257 .update(cx_a, |workspace, cx| {
5258 workspace
5259 .toggle_follow(&ToggleFollow(client_b_id), cx)
5260 .unwrap()
5261 })
5262 .await
5263 .unwrap();
5264 assert_eq!(
5265 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
5266 Some(client_b_id)
5267 );
5268 assert_eq!(
5269 workspace_a.read_with(cx_a, |workspace, cx| workspace
5270 .active_item(cx)
5271 .unwrap()
5272 .id()),
5273 editor_a1.id()
5274 );
5275
5276 // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
5277 let display = MacOSDisplay::new();
5278 active_call_b
5279 .update(cx_b, |call, cx| call.set_location(None, cx))
5280 .await
5281 .unwrap();
5282 active_call_b
5283 .update(cx_b, |call, cx| {
5284 call.room().unwrap().update(cx, |room, cx| {
5285 room.set_display_sources(vec![display.clone()]);
5286 room.share_screen(cx)
5287 })
5288 })
5289 .await
5290 .unwrap();
5291 deterministic.run_until_parked();
5292 let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
5293 workspace
5294 .active_item(cx)
5295 .unwrap()
5296 .downcast::<SharedScreen>()
5297 .unwrap()
5298 });
5299
5300 // Client B activates Zed again, which causes the previous editor to become focused again.
5301 active_call_b
5302 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
5303 .await
5304 .unwrap();
5305 deterministic.run_until_parked();
5306 assert_eq!(
5307 workspace_a.read_with(cx_a, |workspace, cx| workspace
5308 .active_item(cx)
5309 .unwrap()
5310 .id()),
5311 editor_a1.id()
5312 );
5313
5314 // Client B activates an external window again, and the previously-opened screen-sharing item
5315 // gets activated.
5316 active_call_b
5317 .update(cx_b, |call, cx| call.set_location(None, cx))
5318 .await
5319 .unwrap();
5320 deterministic.run_until_parked();
5321 assert_eq!(
5322 workspace_a.read_with(cx_a, |workspace, cx| workspace
5323 .active_item(cx)
5324 .unwrap()
5325 .id()),
5326 shared_screen.id()
5327 );
5328
5329 // Following interrupts when client B disconnects.
5330 client_b.disconnect(&cx_b.to_async()).unwrap();
5331 deterministic.run_until_parked();
5332 assert_eq!(
5333 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
5334 None
5335 );
5336}
5337
5338#[gpui::test(iterations = 10)]
5339async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
5340 cx_a.foreground().forbid_parking();
5341 cx_a.update(editor::init);
5342 cx_b.update(editor::init);
5343
5344 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
5345 let client_a = server.create_client(cx_a, "user_a").await;
5346 let client_b = server.create_client(cx_b, "user_b").await;
5347 server
5348 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5349 .await;
5350 let active_call_a = cx_a.read(ActiveCall::global);
5351 let active_call_b = cx_b.read(ActiveCall::global);
5352
5353 // Client A shares a project.
5354 client_a
5355 .fs
5356 .insert_tree(
5357 "/a",
5358 json!({
5359 "1.txt": "one",
5360 "2.txt": "two",
5361 "3.txt": "three",
5362 "4.txt": "four",
5363 }),
5364 )
5365 .await;
5366 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
5367 active_call_a
5368 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
5369 .await
5370 .unwrap();
5371 let project_id = active_call_a
5372 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5373 .await
5374 .unwrap();
5375
5376 // Client B joins the project.
5377 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5378 active_call_b
5379 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
5380 .await
5381 .unwrap();
5382
5383 // Client A opens some editors.
5384 let workspace_a = client_a.build_workspace(&project_a, cx_a);
5385 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
5386 let _editor_a1 = workspace_a
5387 .update(cx_a, |workspace, cx| {
5388 workspace.open_path((worktree_id, "1.txt"), true, cx)
5389 })
5390 .await
5391 .unwrap()
5392 .downcast::<Editor>()
5393 .unwrap();
5394
5395 // Client B opens an editor.
5396 let workspace_b = client_b.build_workspace(&project_b, cx_b);
5397 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
5398 let _editor_b1 = workspace_b
5399 .update(cx_b, |workspace, cx| {
5400 workspace.open_path((worktree_id, "2.txt"), true, cx)
5401 })
5402 .await
5403 .unwrap()
5404 .downcast::<Editor>()
5405 .unwrap();
5406
5407 // Clients A and B follow each other in split panes
5408 workspace_a.update(cx_a, |workspace, cx| {
5409 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
5410 let pane_a1 = pane_a1.clone();
5411 cx.defer(move |workspace, _| {
5412 assert_ne!(*workspace.active_pane(), pane_a1);
5413 });
5414 });
5415 workspace_a
5416 .update(cx_a, |workspace, cx| {
5417 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
5418 workspace
5419 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
5420 .unwrap()
5421 })
5422 .await
5423 .unwrap();
5424 workspace_b.update(cx_b, |workspace, cx| {
5425 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
5426 let pane_b1 = pane_b1.clone();
5427 cx.defer(move |workspace, _| {
5428 assert_ne!(*workspace.active_pane(), pane_b1);
5429 });
5430 });
5431 workspace_b
5432 .update(cx_b, |workspace, cx| {
5433 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
5434 workspace
5435 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
5436 .unwrap()
5437 })
5438 .await
5439 .unwrap();
5440
5441 workspace_a.update(cx_a, |workspace, cx| {
5442 workspace.activate_next_pane(cx);
5443 });
5444 // Wait for focus effects to be fully flushed
5445 workspace_a.update(cx_a, |workspace, _| {
5446 assert_eq!(*workspace.active_pane(), pane_a1);
5447 });
5448
5449 workspace_a
5450 .update(cx_a, |workspace, cx| {
5451 workspace.open_path((worktree_id, "3.txt"), true, cx)
5452 })
5453 .await
5454 .unwrap();
5455 workspace_b.update(cx_b, |workspace, cx| {
5456 workspace.activate_next_pane(cx);
5457 });
5458
5459 workspace_b
5460 .update(cx_b, |workspace, cx| {
5461 assert_eq!(*workspace.active_pane(), pane_b1);
5462 workspace.open_path((worktree_id, "4.txt"), true, cx)
5463 })
5464 .await
5465 .unwrap();
5466 cx_a.foreground().run_until_parked();
5467
5468 // Ensure leader updates don't change the active pane of followers
5469 workspace_a.read_with(cx_a, |workspace, _| {
5470 assert_eq!(*workspace.active_pane(), pane_a1);
5471 });
5472 workspace_b.read_with(cx_b, |workspace, _| {
5473 assert_eq!(*workspace.active_pane(), pane_b1);
5474 });
5475
5476 // Ensure peers following each other doesn't cause an infinite loop.
5477 assert_eq!(
5478 workspace_a.read_with(cx_a, |workspace, cx| workspace
5479 .active_item(cx)
5480 .unwrap()
5481 .project_path(cx)),
5482 Some((worktree_id, "3.txt").into())
5483 );
5484 workspace_a.update(cx_a, |workspace, cx| {
5485 assert_eq!(
5486 workspace.active_item(cx).unwrap().project_path(cx),
5487 Some((worktree_id, "3.txt").into())
5488 );
5489 workspace.activate_next_pane(cx);
5490 });
5491
5492 workspace_a.update(cx_a, |workspace, cx| {
5493 assert_eq!(
5494 workspace.active_item(cx).unwrap().project_path(cx),
5495 Some((worktree_id, "4.txt").into())
5496 );
5497 });
5498
5499 workspace_b.update(cx_b, |workspace, cx| {
5500 assert_eq!(
5501 workspace.active_item(cx).unwrap().project_path(cx),
5502 Some((worktree_id, "4.txt").into())
5503 );
5504 workspace.activate_next_pane(cx);
5505 });
5506
5507 workspace_b.update(cx_b, |workspace, cx| {
5508 assert_eq!(
5509 workspace.active_item(cx).unwrap().project_path(cx),
5510 Some((worktree_id, "3.txt").into())
5511 );
5512 });
5513}
5514
5515#[gpui::test(iterations = 10)]
5516async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
5517 cx_a.foreground().forbid_parking();
5518 cx_a.update(editor::init);
5519 cx_b.update(editor::init);
5520
5521 // 2 clients connect to a server.
5522 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
5523 let client_a = server.create_client(cx_a, "user_a").await;
5524 let client_b = server.create_client(cx_b, "user_b").await;
5525 server
5526 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5527 .await;
5528 let active_call_a = cx_a.read(ActiveCall::global);
5529 let active_call_b = cx_b.read(ActiveCall::global);
5530
5531 // Client A shares a project.
5532 client_a
5533 .fs
5534 .insert_tree(
5535 "/a",
5536 json!({
5537 "1.txt": "one",
5538 "2.txt": "two",
5539 "3.txt": "three",
5540 }),
5541 )
5542 .await;
5543 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
5544 active_call_a
5545 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
5546 .await
5547 .unwrap();
5548
5549 let project_id = active_call_a
5550 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5551 .await
5552 .unwrap();
5553 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5554 active_call_b
5555 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
5556 .await
5557 .unwrap();
5558
5559 // Client A opens some editors.
5560 let workspace_a = client_a.build_workspace(&project_a, cx_a);
5561 let _editor_a1 = workspace_a
5562 .update(cx_a, |workspace, cx| {
5563 workspace.open_path((worktree_id, "1.txt"), true, cx)
5564 })
5565 .await
5566 .unwrap()
5567 .downcast::<Editor>()
5568 .unwrap();
5569
5570 // Client B starts following client A.
5571 let workspace_b = client_b.build_workspace(&project_b, cx_b);
5572 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
5573 let leader_id = project_b.read_with(cx_b, |project, _| {
5574 project.collaborators().values().next().unwrap().peer_id
5575 });
5576 workspace_b
5577 .update(cx_b, |workspace, cx| {
5578 workspace
5579 .toggle_follow(&ToggleFollow(leader_id), cx)
5580 .unwrap()
5581 })
5582 .await
5583 .unwrap();
5584 assert_eq!(
5585 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5586 Some(leader_id)
5587 );
5588 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
5589 workspace
5590 .active_item(cx)
5591 .unwrap()
5592 .downcast::<Editor>()
5593 .unwrap()
5594 });
5595
5596 // When client B moves, it automatically stops following client A.
5597 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
5598 assert_eq!(
5599 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5600 None
5601 );
5602
5603 workspace_b
5604 .update(cx_b, |workspace, cx| {
5605 workspace
5606 .toggle_follow(&ToggleFollow(leader_id), cx)
5607 .unwrap()
5608 })
5609 .await
5610 .unwrap();
5611 assert_eq!(
5612 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5613 Some(leader_id)
5614 );
5615
5616 // When client B edits, it automatically stops following client A.
5617 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
5618 assert_eq!(
5619 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5620 None
5621 );
5622
5623 workspace_b
5624 .update(cx_b, |workspace, cx| {
5625 workspace
5626 .toggle_follow(&ToggleFollow(leader_id), cx)
5627 .unwrap()
5628 })
5629 .await
5630 .unwrap();
5631 assert_eq!(
5632 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5633 Some(leader_id)
5634 );
5635
5636 // When client B scrolls, it automatically stops following client A.
5637 editor_b2.update(cx_b, |editor, cx| {
5638 editor.set_scroll_position(vec2f(0., 3.), cx)
5639 });
5640 assert_eq!(
5641 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5642 None
5643 );
5644
5645 workspace_b
5646 .update(cx_b, |workspace, cx| {
5647 workspace
5648 .toggle_follow(&ToggleFollow(leader_id), cx)
5649 .unwrap()
5650 })
5651 .await
5652 .unwrap();
5653 assert_eq!(
5654 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5655 Some(leader_id)
5656 );
5657
5658 // When client B activates a different pane, it continues following client A in the original pane.
5659 workspace_b.update(cx_b, |workspace, cx| {
5660 workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
5661 });
5662 assert_eq!(
5663 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5664 Some(leader_id)
5665 );
5666
5667 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
5668 assert_eq!(
5669 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5670 Some(leader_id)
5671 );
5672
5673 // When client B activates a different item in the original pane, it automatically stops following client A.
5674 workspace_b
5675 .update(cx_b, |workspace, cx| {
5676 workspace.open_path((worktree_id, "2.txt"), true, cx)
5677 })
5678 .await
5679 .unwrap();
5680 assert_eq!(
5681 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5682 None
5683 );
5684}
5685
5686#[gpui::test(iterations = 10)]
5687async fn test_peers_simultaneously_following_each_other(
5688 deterministic: Arc<Deterministic>,
5689 cx_a: &mut TestAppContext,
5690 cx_b: &mut TestAppContext,
5691) {
5692 deterministic.forbid_parking();
5693 cx_a.update(editor::init);
5694 cx_b.update(editor::init);
5695
5696 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
5697 let client_a = server.create_client(cx_a, "user_a").await;
5698 let client_b = server.create_client(cx_b, "user_b").await;
5699 server
5700 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5701 .await;
5702 let active_call_a = cx_a.read(ActiveCall::global);
5703
5704 client_a.fs.insert_tree("/a", json!({})).await;
5705 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
5706 let workspace_a = client_a.build_workspace(&project_a, cx_a);
5707 let project_id = active_call_a
5708 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5709 .await
5710 .unwrap();
5711
5712 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5713 let workspace_b = client_b.build_workspace(&project_b, cx_b);
5714
5715 deterministic.run_until_parked();
5716 let client_a_id = project_b.read_with(cx_b, |project, _| {
5717 project.collaborators().values().next().unwrap().peer_id
5718 });
5719 let client_b_id = project_a.read_with(cx_a, |project, _| {
5720 project.collaborators().values().next().unwrap().peer_id
5721 });
5722
5723 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
5724 workspace
5725 .toggle_follow(&ToggleFollow(client_b_id), cx)
5726 .unwrap()
5727 });
5728 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
5729 workspace
5730 .toggle_follow(&ToggleFollow(client_a_id), cx)
5731 .unwrap()
5732 });
5733
5734 futures::try_join!(a_follow_b, b_follow_a).unwrap();
5735 workspace_a.read_with(cx_a, |workspace, _| {
5736 assert_eq!(
5737 workspace.leader_for_pane(workspace.active_pane()),
5738 Some(client_b_id)
5739 );
5740 });
5741 workspace_b.read_with(cx_b, |workspace, _| {
5742 assert_eq!(
5743 workspace.leader_for_pane(workspace.active_pane()),
5744 Some(client_a_id)
5745 );
5746 });
5747}
5748
5749#[gpui::test(iterations = 100)]
5750async fn test_random_collaboration(
5751 cx: &mut TestAppContext,
5752 deterministic: Arc<Deterministic>,
5753 rng: StdRng,
5754) {
5755 deterministic.forbid_parking();
5756 let max_peers = env::var("MAX_PEERS")
5757 .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
5758 .unwrap_or(5);
5759 assert!(max_peers <= 5);
5760
5761 let max_operations = env::var("OPERATIONS")
5762 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
5763 .unwrap_or(10);
5764
5765 let rng = Arc::new(Mutex::new(rng));
5766
5767 let guest_lang_registry = Arc::new(LanguageRegistry::test());
5768 let host_language_registry = Arc::new(LanguageRegistry::test());
5769
5770 let fs = FakeFs::new(cx.background());
5771 fs.insert_tree("/_collab", json!({"init": ""})).await;
5772
5773 let mut server = TestServer::start(cx.foreground(), cx.background()).await;
5774 let db = server.app_state.db.clone();
5775
5776 let room_creator_user_id = db
5777 .create_user(
5778 "room-creator@example.com",
5779 false,
5780 NewUserParams {
5781 github_login: "room-creator".into(),
5782 github_user_id: 0,
5783 invite_count: 0,
5784 },
5785 )
5786 .await
5787 .unwrap()
5788 .user_id;
5789 let mut available_guests = vec![
5790 "guest-1".to_string(),
5791 "guest-2".to_string(),
5792 "guest-3".to_string(),
5793 "guest-4".to_string(),
5794 ];
5795
5796 for (ix, username) in Some(&"host".to_string())
5797 .into_iter()
5798 .chain(&available_guests)
5799 .enumerate()
5800 {
5801 let user_id = db
5802 .create_user(
5803 &format!("{username}@example.com"),
5804 false,
5805 NewUserParams {
5806 github_login: username.into(),
5807 github_user_id: (ix + 1) as i32,
5808 invite_count: 0,
5809 },
5810 )
5811 .await
5812 .unwrap()
5813 .user_id;
5814 server
5815 .app_state
5816 .db
5817 .send_contact_request(user_id, room_creator_user_id)
5818 .await
5819 .unwrap();
5820 server
5821 .app_state
5822 .db
5823 .respond_to_contact_request(room_creator_user_id, user_id, true)
5824 .await
5825 .unwrap();
5826 }
5827
5828 let _room_creator = server.create_client(cx, "room-creator").await;
5829 let active_call = cx.read(ActiveCall::global);
5830
5831 let mut clients = Vec::new();
5832 let mut user_ids = Vec::new();
5833 let mut peer_ids = Vec::new();
5834 let mut op_start_signals = Vec::new();
5835
5836 let mut next_entity_id = 100000;
5837 let mut host_cx = TestAppContext::new(
5838 cx.foreground_platform(),
5839 cx.platform(),
5840 deterministic.build_foreground(next_entity_id),
5841 deterministic.build_background(),
5842 cx.font_cache(),
5843 cx.leak_detector(),
5844 next_entity_id,
5845 cx.function_name.clone(),
5846 );
5847 let host = server.create_client(&mut host_cx, "host").await;
5848 let host_project = host_cx.update(|cx| {
5849 Project::local(
5850 host.client.clone(),
5851 host.user_store.clone(),
5852 host.project_store.clone(),
5853 host_language_registry.clone(),
5854 fs.clone(),
5855 cx,
5856 )
5857 });
5858
5859 let (collab_worktree, _) = host_project
5860 .update(&mut host_cx, |project, cx| {
5861 project.find_or_create_local_worktree("/_collab", true, cx)
5862 })
5863 .await
5864 .unwrap();
5865 collab_worktree
5866 .read_with(&host_cx, |tree, _| tree.as_local().unwrap().scan_complete())
5867 .await;
5868
5869 // Set up fake language servers.
5870 let mut language = Language::new(
5871 LanguageConfig {
5872 name: "Rust".into(),
5873 path_suffixes: vec!["rs".to_string()],
5874 ..Default::default()
5875 },
5876 None,
5877 );
5878 let _fake_servers = language
5879 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5880 name: "the-fake-language-server",
5881 capabilities: lsp::LanguageServer::full_capabilities(),
5882 initializer: Some(Box::new({
5883 let rng = rng.clone();
5884 let fs = fs.clone();
5885 let project = host_project.downgrade();
5886 move |fake_server: &mut FakeLanguageServer| {
5887 fake_server.handle_request::<lsp::request::Completion, _, _>(
5888 |_, _| async move {
5889 Ok(Some(lsp::CompletionResponse::Array(vec![
5890 lsp::CompletionItem {
5891 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
5892 range: lsp::Range::new(
5893 lsp::Position::new(0, 0),
5894 lsp::Position::new(0, 0),
5895 ),
5896 new_text: "the-new-text".to_string(),
5897 })),
5898 ..Default::default()
5899 },
5900 ])))
5901 },
5902 );
5903
5904 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
5905 |_, _| async move {
5906 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
5907 lsp::CodeAction {
5908 title: "the-code-action".to_string(),
5909 ..Default::default()
5910 },
5911 )]))
5912 },
5913 );
5914
5915 fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
5916 |params, _| async move {
5917 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
5918 params.position,
5919 params.position,
5920 ))))
5921 },
5922 );
5923
5924 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
5925 let fs = fs.clone();
5926 let rng = rng.clone();
5927 move |_, _| {
5928 let fs = fs.clone();
5929 let rng = rng.clone();
5930 async move {
5931 let files = fs.files().await;
5932 let mut rng = rng.lock();
5933 let count = rng.gen_range::<usize, _>(1..3);
5934 let files = (0..count)
5935 .map(|_| files.choose(&mut *rng).unwrap())
5936 .collect::<Vec<_>>();
5937 log::info!("LSP: Returning definitions in files {:?}", &files);
5938 Ok(Some(lsp::GotoDefinitionResponse::Array(
5939 files
5940 .into_iter()
5941 .map(|file| lsp::Location {
5942 uri: lsp::Url::from_file_path(file).unwrap(),
5943 range: Default::default(),
5944 })
5945 .collect(),
5946 )))
5947 }
5948 }
5949 });
5950
5951 fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
5952 let rng = rng.clone();
5953 let project = project;
5954 move |params, mut cx| {
5955 let highlights = if let Some(project) = project.upgrade(&cx) {
5956 project.update(&mut cx, |project, cx| {
5957 let path = params
5958 .text_document_position_params
5959 .text_document
5960 .uri
5961 .to_file_path()
5962 .unwrap();
5963 let (worktree, relative_path) =
5964 project.find_local_worktree(&path, cx)?;
5965 let project_path =
5966 ProjectPath::from((worktree.read(cx).id(), relative_path));
5967 let buffer =
5968 project.get_open_buffer(&project_path, cx)?.read(cx);
5969
5970 let mut highlights = Vec::new();
5971 let highlight_count = rng.lock().gen_range(1..=5);
5972 let mut prev_end = 0;
5973 for _ in 0..highlight_count {
5974 let range =
5975 buffer.random_byte_range(prev_end, &mut *rng.lock());
5976
5977 highlights.push(lsp::DocumentHighlight {
5978 range: range_to_lsp(range.to_point_utf16(buffer)),
5979 kind: Some(lsp::DocumentHighlightKind::READ),
5980 });
5981 prev_end = range.end;
5982 }
5983 Some(highlights)
5984 })
5985 } else {
5986 None
5987 };
5988 async move { Ok(highlights) }
5989 }
5990 });
5991 }
5992 })),
5993 ..Default::default()
5994 }))
5995 .await;
5996 host_language_registry.add(Arc::new(language));
5997
5998 let host_user_id = host.current_user_id(&host_cx);
5999 active_call
6000 .update(cx, |call, cx| {
6001 call.invite(host_user_id.to_proto(), None, cx)
6002 })
6003 .await
6004 .unwrap();
6005 active_call.read_with(cx, |call, cx| call.room().unwrap().read(cx).id());
6006 deterministic.run_until_parked();
6007 let host_active_call = host_cx.read(ActiveCall::global);
6008 host_active_call
6009 .update(&mut host_cx, |call, cx| call.accept_incoming(cx))
6010 .await
6011 .unwrap();
6012
6013 let host_project_id = host_active_call
6014 .update(&mut host_cx, |call, cx| {
6015 call.share_project(host_project.clone(), cx)
6016 })
6017 .await
6018 .unwrap();
6019
6020 let op_start_signal = futures::channel::mpsc::unbounded();
6021 user_ids.push(host_user_id);
6022 peer_ids.push(host.peer_id().unwrap());
6023 op_start_signals.push(op_start_signal.0);
6024 clients.push(host_cx.foreground().spawn(host.simulate_host(
6025 host_project,
6026 op_start_signal.1,
6027 rng.clone(),
6028 host_cx,
6029 )));
6030
6031 let disconnect_host_at = if rng.lock().gen_bool(0.2) {
6032 rng.lock().gen_range(0..max_operations)
6033 } else {
6034 max_operations
6035 };
6036
6037 let mut operations = 0;
6038 while operations < max_operations {
6039 if operations == disconnect_host_at {
6040 server.disconnect_client(peer_ids[0]);
6041 deterministic.advance_clock(RECEIVE_TIMEOUT);
6042 drop(op_start_signals);
6043
6044 deterministic.start_waiting();
6045 let mut clients = futures::future::join_all(clients).await;
6046 deterministic.finish_waiting();
6047 deterministic.run_until_parked();
6048
6049 let (host, host_project, mut host_cx, host_err) = clients.remove(0);
6050 if let Some(host_err) = host_err {
6051 log::error!("host error - {:?}", host_err);
6052 }
6053 host_project.read_with(&host_cx, |project, _| assert!(!project.is_shared()));
6054 for (guest, guest_project, mut guest_cx, guest_err) in clients {
6055 if let Some(guest_err) = guest_err {
6056 log::error!("{} error - {:?}", guest.username, guest_err);
6057 }
6058
6059 guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
6060 guest_cx.update(|cx| {
6061 cx.clear_globals();
6062 drop((guest, guest_project));
6063 });
6064 }
6065 host_cx.update(|cx| {
6066 cx.clear_globals();
6067 drop((host, host_project));
6068 });
6069
6070 return;
6071 }
6072
6073 let distribution = rng.lock().gen_range(0..100);
6074 match distribution {
6075 0..=19 if !available_guests.is_empty() => {
6076 let guest_ix = rng.lock().gen_range(0..available_guests.len());
6077 let guest_username = available_guests.remove(guest_ix);
6078 log::info!("Adding new connection for {}", guest_username);
6079 next_entity_id += 100000;
6080 let mut guest_cx = TestAppContext::new(
6081 cx.foreground_platform(),
6082 cx.platform(),
6083 deterministic.build_foreground(next_entity_id),
6084 deterministic.build_background(),
6085 cx.font_cache(),
6086 cx.leak_detector(),
6087 next_entity_id,
6088 cx.function_name.clone(),
6089 );
6090
6091 deterministic.start_waiting();
6092 let guest = server.create_client(&mut guest_cx, &guest_username).await;
6093 let guest_user_id = guest.current_user_id(&guest_cx);
6094
6095 active_call
6096 .update(cx, |call, cx| {
6097 call.invite(guest_user_id.to_proto(), None, cx)
6098 })
6099 .await
6100 .unwrap();
6101 deterministic.run_until_parked();
6102 guest_cx
6103 .read(ActiveCall::global)
6104 .update(&mut guest_cx, |call, cx| call.accept_incoming(cx))
6105 .await
6106 .unwrap();
6107
6108 let guest_project = Project::remote(
6109 host_project_id,
6110 guest.client.clone(),
6111 guest.user_store.clone(),
6112 guest.project_store.clone(),
6113 guest_lang_registry.clone(),
6114 FakeFs::new(cx.background()),
6115 guest_cx.to_async(),
6116 )
6117 .await
6118 .unwrap();
6119 deterministic.finish_waiting();
6120
6121 let op_start_signal = futures::channel::mpsc::unbounded();
6122 user_ids.push(guest_user_id);
6123 peer_ids.push(guest.peer_id().unwrap());
6124 op_start_signals.push(op_start_signal.0);
6125 clients.push(guest_cx.foreground().spawn(guest.simulate_guest(
6126 guest_username.clone(),
6127 guest_project,
6128 op_start_signal.1,
6129 rng.clone(),
6130 guest_cx,
6131 )));
6132
6133 log::info!("Added connection for {}", guest_username);
6134 operations += 1;
6135 }
6136 20..=29 if clients.len() > 1 => {
6137 let guest_ix = rng.lock().gen_range(1..clients.len());
6138 log::info!("Removing guest {}", user_ids[guest_ix]);
6139 let removed_guest_id = user_ids.remove(guest_ix);
6140 let removed_peer_id = peer_ids.remove(guest_ix);
6141 let guest = clients.remove(guest_ix);
6142 op_start_signals.remove(guest_ix);
6143 server.forbid_connections();
6144 server.disconnect_client(removed_peer_id);
6145 deterministic.advance_clock(RECEIVE_TIMEOUT);
6146 deterministic.start_waiting();
6147 log::info!("Waiting for guest {} to exit...", removed_guest_id);
6148 let (guest, guest_project, mut guest_cx, guest_err) = guest.await;
6149 deterministic.finish_waiting();
6150 server.allow_connections();
6151
6152 if let Some(guest_err) = guest_err {
6153 log::error!("{} error - {:?}", guest.username, guest_err);
6154 }
6155 guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
6156 for user_id in &user_ids {
6157 let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
6158 let contacts = server
6159 .store
6160 .lock()
6161 .await
6162 .build_initial_contacts_update(contacts)
6163 .contacts;
6164 for contact in contacts {
6165 if contact.online {
6166 assert_ne!(
6167 contact.user_id, removed_guest_id.0 as u64,
6168 "removed guest is still a contact of another peer"
6169 );
6170 }
6171 }
6172 }
6173
6174 log::info!("{} removed", guest.username);
6175 available_guests.push(guest.username.clone());
6176 guest_cx.update(|cx| {
6177 cx.clear_globals();
6178 drop((guest, guest_project));
6179 });
6180
6181 operations += 1;
6182 }
6183 _ => {
6184 while operations < max_operations && rng.lock().gen_bool(0.7) {
6185 op_start_signals
6186 .choose(&mut *rng.lock())
6187 .unwrap()
6188 .unbounded_send(())
6189 .unwrap();
6190 operations += 1;
6191 }
6192
6193 if rng.lock().gen_bool(0.8) {
6194 deterministic.run_until_parked();
6195 }
6196 }
6197 }
6198 }
6199
6200 drop(op_start_signals);
6201 deterministic.start_waiting();
6202 let mut clients = futures::future::join_all(clients).await;
6203 deterministic.finish_waiting();
6204 deterministic.run_until_parked();
6205
6206 let (host_client, host_project, mut host_cx, host_err) = clients.remove(0);
6207 if let Some(host_err) = host_err {
6208 panic!("host error - {:?}", host_err);
6209 }
6210 let host_worktree_snapshots = host_project.read_with(&host_cx, |project, cx| {
6211 project
6212 .worktrees(cx)
6213 .map(|worktree| {
6214 let snapshot = worktree.read(cx).snapshot();
6215 (snapshot.id(), snapshot)
6216 })
6217 .collect::<BTreeMap<_, _>>()
6218 });
6219
6220 host_project.read_with(&host_cx, |project, cx| project.check_invariants(cx));
6221
6222 for (guest_client, guest_project, mut guest_cx, guest_err) in clients.into_iter() {
6223 if let Some(guest_err) = guest_err {
6224 panic!("{} error - {:?}", guest_client.username, guest_err);
6225 }
6226 let worktree_snapshots = guest_project.read_with(&guest_cx, |project, cx| {
6227 project
6228 .worktrees(cx)
6229 .map(|worktree| {
6230 let worktree = worktree.read(cx);
6231 (worktree.id(), worktree.snapshot())
6232 })
6233 .collect::<BTreeMap<_, _>>()
6234 });
6235
6236 assert_eq!(
6237 worktree_snapshots.keys().collect::<Vec<_>>(),
6238 host_worktree_snapshots.keys().collect::<Vec<_>>(),
6239 "{} has different worktrees than the host",
6240 guest_client.username
6241 );
6242 for (id, host_snapshot) in &host_worktree_snapshots {
6243 let guest_snapshot = &worktree_snapshots[id];
6244 assert_eq!(
6245 guest_snapshot.root_name(),
6246 host_snapshot.root_name(),
6247 "{} has different root name than the host for worktree {}",
6248 guest_client.username,
6249 id
6250 );
6251 assert_eq!(
6252 guest_snapshot.entries(false).collect::<Vec<_>>(),
6253 host_snapshot.entries(false).collect::<Vec<_>>(),
6254 "{} has different snapshot than the host for worktree {}",
6255 guest_client.username,
6256 id
6257 );
6258 assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id());
6259 }
6260
6261 guest_project.read_with(&guest_cx, |project, cx| project.check_invariants(cx));
6262
6263 for guest_buffer in &guest_client.buffers {
6264 let buffer_id = guest_buffer.read_with(&guest_cx, |buffer, _| buffer.remote_id());
6265 let host_buffer = host_project.read_with(&host_cx, |project, cx| {
6266 project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
6267 panic!(
6268 "host does not have buffer for guest:{}, peer:{:?}, id:{}",
6269 guest_client.username,
6270 guest_client.peer_id(),
6271 buffer_id
6272 )
6273 })
6274 });
6275 let path =
6276 host_buffer.read_with(&host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
6277
6278 assert_eq!(
6279 guest_buffer.read_with(&guest_cx, |buffer, _| buffer.deferred_ops_len()),
6280 0,
6281 "{}, buffer {}, path {:?} has deferred operations",
6282 guest_client.username,
6283 buffer_id,
6284 path,
6285 );
6286 assert_eq!(
6287 guest_buffer.read_with(&guest_cx, |buffer, _| buffer.text()),
6288 host_buffer.read_with(&host_cx, |buffer, _| buffer.text()),
6289 "{}, buffer {}, path {:?}, differs from the host's buffer",
6290 guest_client.username,
6291 buffer_id,
6292 path
6293 );
6294 }
6295
6296 guest_cx.update(|cx| {
6297 cx.clear_globals();
6298 drop((guest_project, guest_client));
6299 });
6300 }
6301
6302 host_cx.update(|cx| {
6303 cx.clear_globals();
6304 drop((host_client, host_project))
6305 });
6306}
6307
6308struct TestServer {
6309 peer: Arc<Peer>,
6310 app_state: Arc<AppState>,
6311 server: Arc<Server>,
6312 foreground: Rc<executor::Foreground>,
6313 notifications: mpsc::UnboundedReceiver<()>,
6314 connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
6315 forbid_connections: Arc<AtomicBool>,
6316 _test_db: TestDb,
6317 test_live_kit_server: Arc<live_kit_client::TestServer>,
6318}
6319
6320impl TestServer {
6321 async fn start(
6322 foreground: Rc<executor::Foreground>,
6323 background: Arc<executor::Background>,
6324 ) -> Self {
6325 static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
6326
6327 let test_db = TestDb::fake(background.clone());
6328 let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst);
6329 let live_kit_server = live_kit_client::TestServer::create(
6330 format!("http://livekit.{}.test", live_kit_server_id),
6331 format!("devkey-{}", live_kit_server_id),
6332 format!("secret-{}", live_kit_server_id),
6333 background.clone(),
6334 )
6335 .unwrap();
6336 let app_state = Self::build_app_state(&test_db, &live_kit_server).await;
6337 let peer = Peer::new();
6338 let notifications = mpsc::unbounded();
6339 let server = Server::new(app_state.clone(), Some(notifications.0));
6340 Self {
6341 peer,
6342 app_state,
6343 server,
6344 foreground,
6345 notifications: notifications.1,
6346 connection_killers: Default::default(),
6347 forbid_connections: Default::default(),
6348 _test_db: test_db,
6349 test_live_kit_server: live_kit_server,
6350 }
6351 }
6352
6353 async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
6354 cx.update(|cx| {
6355 cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf()));
6356
6357 let mut settings = Settings::test(cx);
6358 settings.projects_online_by_default = false;
6359 cx.set_global(settings);
6360 });
6361
6362 let http = FakeHttpClient::with_404_response();
6363 let user_id = if let Ok(Some(user)) = self
6364 .app_state
6365 .db
6366 .get_user_by_github_account(name, None)
6367 .await
6368 {
6369 user.id
6370 } else {
6371 self.app_state
6372 .db
6373 .create_user(
6374 &format!("{name}@example.com"),
6375 false,
6376 NewUserParams {
6377 github_login: name.into(),
6378 github_user_id: 0,
6379 invite_count: 0,
6380 },
6381 )
6382 .await
6383 .unwrap()
6384 .user_id
6385 };
6386 let client_name = name.to_string();
6387 let mut client = cx.read(|cx| Client::new(http.clone(), cx));
6388 let server = self.server.clone();
6389 let db = self.app_state.db.clone();
6390 let connection_killers = self.connection_killers.clone();
6391 let forbid_connections = self.forbid_connections.clone();
6392
6393 Arc::get_mut(&mut client)
6394 .unwrap()
6395 .set_id(user_id.0 as usize)
6396 .override_authenticate(move |cx| {
6397 cx.spawn(|_| async move {
6398 let access_token = "the-token".to_string();
6399 Ok(Credentials {
6400 user_id: user_id.0 as u64,
6401 access_token,
6402 })
6403 })
6404 })
6405 .override_establish_connection(move |credentials, cx| {
6406 assert_eq!(credentials.user_id, user_id.0 as u64);
6407 assert_eq!(credentials.access_token, "the-token");
6408
6409 let server = server.clone();
6410 let db = db.clone();
6411 let connection_killers = connection_killers.clone();
6412 let forbid_connections = forbid_connections.clone();
6413 let client_name = client_name.clone();
6414 cx.spawn(move |cx| async move {
6415 if forbid_connections.load(SeqCst) {
6416 Err(EstablishConnectionError::other(anyhow!(
6417 "server is forbidding connections"
6418 )))
6419 } else {
6420 let (client_conn, server_conn, killed) =
6421 Connection::in_memory(cx.background());
6422 let (connection_id_tx, connection_id_rx) = oneshot::channel();
6423 let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
6424 cx.background()
6425 .spawn(server.handle_connection(
6426 server_conn,
6427 client_name,
6428 user,
6429 Some(connection_id_tx),
6430 cx.background(),
6431 ))
6432 .detach();
6433 let connection_id = connection_id_rx.await.unwrap();
6434 connection_killers
6435 .lock()
6436 .insert(PeerId(connection_id.0), killed);
6437 Ok(client_conn)
6438 }
6439 })
6440 });
6441
6442 let fs = FakeFs::new(cx.background());
6443 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
6444 let project_store = cx.add_model(|_| ProjectStore::new());
6445 let app_state = Arc::new(workspace::AppState {
6446 client: client.clone(),
6447 user_store: user_store.clone(),
6448 project_store: project_store.clone(),
6449 languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
6450 themes: ThemeRegistry::new((), cx.font_cache()),
6451 fs: fs.clone(),
6452 build_window_options: Default::default,
6453 initialize_workspace: |_, _, _| unimplemented!(),
6454 default_item_factory: |_, _| unimplemented!(),
6455 });
6456
6457 Channel::init(&client);
6458 Project::init(&client);
6459 cx.update(|cx| {
6460 workspace::init(app_state.clone(), cx);
6461 call::init(client.clone(), user_store.clone(), cx);
6462 });
6463
6464 client
6465 .authenticate_and_connect(false, &cx.to_async())
6466 .await
6467 .unwrap();
6468
6469 let client = TestClient {
6470 client,
6471 username: name.to_string(),
6472 user_store,
6473 project_store,
6474 fs,
6475 language_registry: Arc::new(LanguageRegistry::test()),
6476 buffers: Default::default(),
6477 };
6478 client.wait_for_current_user(cx).await;
6479 client
6480 }
6481
6482 fn disconnect_client(&self, peer_id: PeerId) {
6483 self.connection_killers
6484 .lock()
6485 .remove(&peer_id)
6486 .unwrap()
6487 .store(true, SeqCst);
6488 }
6489
6490 fn forbid_connections(&self) {
6491 self.forbid_connections.store(true, SeqCst);
6492 }
6493
6494 fn allow_connections(&self) {
6495 self.forbid_connections.store(false, SeqCst);
6496 }
6497
6498 async fn make_contacts(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
6499 for ix in 1..clients.len() {
6500 let (left, right) = clients.split_at_mut(ix);
6501 let (client_a, cx_a) = left.last_mut().unwrap();
6502 for (client_b, cx_b) in right {
6503 client_a
6504 .user_store
6505 .update(*cx_a, |store, cx| {
6506 store.request_contact(client_b.user_id().unwrap(), cx)
6507 })
6508 .await
6509 .unwrap();
6510 cx_a.foreground().run_until_parked();
6511 client_b
6512 .user_store
6513 .update(*cx_b, |store, cx| {
6514 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
6515 })
6516 .await
6517 .unwrap();
6518 }
6519 }
6520 }
6521
6522 async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
6523 self.make_contacts(clients).await;
6524
6525 let (left, right) = clients.split_at_mut(1);
6526 let (_client_a, cx_a) = &mut left[0];
6527 let active_call_a = cx_a.read(ActiveCall::global);
6528
6529 for (client_b, cx_b) in right {
6530 let user_id_b = client_b.current_user_id(*cx_b).to_proto();
6531 active_call_a
6532 .update(*cx_a, |call, cx| call.invite(user_id_b, None, cx))
6533 .await
6534 .unwrap();
6535
6536 cx_b.foreground().run_until_parked();
6537 let active_call_b = cx_b.read(ActiveCall::global);
6538 active_call_b
6539 .update(*cx_b, |call, cx| call.accept_incoming(cx))
6540 .await
6541 .unwrap();
6542 }
6543 }
6544
6545 async fn build_app_state(
6546 test_db: &TestDb,
6547 fake_server: &live_kit_client::TestServer,
6548 ) -> Arc<AppState> {
6549 Arc::new(AppState {
6550 db: test_db.db().clone(),
6551 live_kit_client: Some(Arc::new(fake_server.create_api_client())),
6552 config: Default::default(),
6553 })
6554 }
6555
6556 async fn condition<F>(&mut self, mut predicate: F)
6557 where
6558 F: FnMut(&Store) -> bool,
6559 {
6560 assert!(
6561 self.foreground.parking_forbidden(),
6562 "you must call forbid_parking to use server conditions so we don't block indefinitely"
6563 );
6564 while !(predicate)(&*self.server.store.lock().await) {
6565 self.foreground.start_waiting();
6566 self.notifications.next().await;
6567 self.foreground.finish_waiting();
6568 }
6569 }
6570}
6571
6572impl Deref for TestServer {
6573 type Target = Server;
6574
6575 fn deref(&self) -> &Self::Target {
6576 &self.server
6577 }
6578}
6579
6580impl Drop for TestServer {
6581 fn drop(&mut self) {
6582 self.peer.reset();
6583 self.test_live_kit_server.teardown().unwrap();
6584 }
6585}
6586
6587struct TestClient {
6588 client: Arc<Client>,
6589 username: String,
6590 pub user_store: ModelHandle<UserStore>,
6591 pub project_store: ModelHandle<ProjectStore>,
6592 language_registry: Arc<LanguageRegistry>,
6593 fs: Arc<FakeFs>,
6594 buffers: HashSet<ModelHandle<language::Buffer>>,
6595}
6596
6597impl Deref for TestClient {
6598 type Target = Arc<Client>;
6599
6600 fn deref(&self) -> &Self::Target {
6601 &self.client
6602 }
6603}
6604
6605struct ContactsSummary {
6606 pub current: Vec<String>,
6607 pub outgoing_requests: Vec<String>,
6608 pub incoming_requests: Vec<String>,
6609}
6610
6611impl TestClient {
6612 pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
6613 UserId::from_proto(
6614 self.user_store
6615 .read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
6616 )
6617 }
6618
6619 async fn wait_for_current_user(&self, cx: &TestAppContext) {
6620 let mut authed_user = self
6621 .user_store
6622 .read_with(cx, |user_store, _| user_store.watch_current_user());
6623 while authed_user.next().await.unwrap().is_none() {}
6624 }
6625
6626 async fn clear_contacts(&self, cx: &mut TestAppContext) {
6627 self.user_store
6628 .update(cx, |store, _| store.clear_contacts())
6629 .await;
6630 }
6631
6632 fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
6633 self.user_store.read_with(cx, |store, _| ContactsSummary {
6634 current: store
6635 .contacts()
6636 .iter()
6637 .map(|contact| contact.user.github_login.clone())
6638 .collect(),
6639 outgoing_requests: store
6640 .outgoing_contact_requests()
6641 .iter()
6642 .map(|user| user.github_login.clone())
6643 .collect(),
6644 incoming_requests: store
6645 .incoming_contact_requests()
6646 .iter()
6647 .map(|user| user.github_login.clone())
6648 .collect(),
6649 })
6650 }
6651
6652 async fn build_local_project(
6653 &self,
6654 root_path: impl AsRef<Path>,
6655 cx: &mut TestAppContext,
6656 ) -> (ModelHandle<Project>, WorktreeId) {
6657 let project = cx.update(|cx| {
6658 Project::local(
6659 self.client.clone(),
6660 self.user_store.clone(),
6661 self.project_store.clone(),
6662 self.language_registry.clone(),
6663 self.fs.clone(),
6664 cx,
6665 )
6666 });
6667 let (worktree, _) = project
6668 .update(cx, |p, cx| {
6669 p.find_or_create_local_worktree(root_path, true, cx)
6670 })
6671 .await
6672 .unwrap();
6673 worktree
6674 .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
6675 .await;
6676 (project, worktree.read_with(cx, |tree, _| tree.id()))
6677 }
6678
6679 async fn build_remote_project(
6680 &self,
6681 host_project_id: u64,
6682 guest_cx: &mut TestAppContext,
6683 ) -> ModelHandle<Project> {
6684 let project_b = guest_cx.spawn(|cx| {
6685 Project::remote(
6686 host_project_id,
6687 self.client.clone(),
6688 self.user_store.clone(),
6689 self.project_store.clone(),
6690 self.language_registry.clone(),
6691 FakeFs::new(cx.background()),
6692 cx,
6693 )
6694 });
6695 project_b.await.unwrap()
6696 }
6697
6698 fn build_workspace(
6699 &self,
6700 project: &ModelHandle<Project>,
6701 cx: &mut TestAppContext,
6702 ) -> ViewHandle<Workspace> {
6703 let (_, root_view) = cx.add_window(|_| EmptyView);
6704 cx.add_view(&root_view, |cx| {
6705 Workspace::new(project.clone(), |_, _| unimplemented!(), cx)
6706 })
6707 }
6708
6709 async fn simulate_host(
6710 mut self,
6711 project: ModelHandle<Project>,
6712 op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
6713 rng: Arc<Mutex<StdRng>>,
6714 mut cx: TestAppContext,
6715 ) -> (
6716 Self,
6717 ModelHandle<Project>,
6718 TestAppContext,
6719 Option<anyhow::Error>,
6720 ) {
6721 async fn simulate_host_internal(
6722 client: &mut TestClient,
6723 project: ModelHandle<Project>,
6724 mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
6725 rng: Arc<Mutex<StdRng>>,
6726 cx: &mut TestAppContext,
6727 ) -> anyhow::Result<()> {
6728 let fs = project.read_with(cx, |project, _| project.fs().clone());
6729
6730 while op_start_signal.next().await.is_some() {
6731 let distribution = rng.lock().gen_range::<usize, _>(0..100);
6732 let files = fs.as_fake().files().await;
6733 match distribution {
6734 0..=19 if !files.is_empty() => {
6735 let path = files.choose(&mut *rng.lock()).unwrap();
6736 let mut path = path.as_path();
6737 while let Some(parent_path) = path.parent() {
6738 path = parent_path;
6739 if rng.lock().gen() {
6740 break;
6741 }
6742 }
6743
6744 log::info!("Host: find/create local worktree {:?}", path);
6745 let find_or_create_worktree = project.update(cx, |project, cx| {
6746 project.find_or_create_local_worktree(path, true, cx)
6747 });
6748 if rng.lock().gen() {
6749 cx.background().spawn(find_or_create_worktree).detach();
6750 } else {
6751 find_or_create_worktree.await?;
6752 }
6753 }
6754 20..=79 if !files.is_empty() => {
6755 let buffer = if client.buffers.is_empty() || rng.lock().gen() {
6756 let file = files.choose(&mut *rng.lock()).unwrap();
6757 let (worktree, path) = project
6758 .update(cx, |project, cx| {
6759 project.find_or_create_local_worktree(file.clone(), true, cx)
6760 })
6761 .await?;
6762 let project_path =
6763 worktree.read_with(cx, |worktree, _| (worktree.id(), path));
6764 log::info!(
6765 "Host: opening path {:?}, worktree {}, relative_path {:?}",
6766 file,
6767 project_path.0,
6768 project_path.1
6769 );
6770 let buffer = project
6771 .update(cx, |project, cx| project.open_buffer(project_path, cx))
6772 .await
6773 .unwrap();
6774 client.buffers.insert(buffer.clone());
6775 buffer
6776 } else {
6777 client
6778 .buffers
6779 .iter()
6780 .choose(&mut *rng.lock())
6781 .unwrap()
6782 .clone()
6783 };
6784
6785 if rng.lock().gen_bool(0.1) {
6786 cx.update(|cx| {
6787 log::info!(
6788 "Host: dropping buffer {:?}",
6789 buffer.read(cx).file().unwrap().full_path(cx)
6790 );
6791 client.buffers.remove(&buffer);
6792 drop(buffer);
6793 });
6794 } else {
6795 buffer.update(cx, |buffer, cx| {
6796 log::info!(
6797 "Host: updating buffer {:?} ({})",
6798 buffer.file().unwrap().full_path(cx),
6799 buffer.remote_id()
6800 );
6801
6802 if rng.lock().gen_bool(0.7) {
6803 buffer.randomly_edit(&mut *rng.lock(), 5, cx);
6804 } else {
6805 buffer.randomly_undo_redo(&mut *rng.lock(), cx);
6806 }
6807 });
6808 }
6809 }
6810 _ => loop {
6811 let path_component_count = rng.lock().gen_range::<usize, _>(1..=5);
6812 let mut path = PathBuf::new();
6813 path.push("/");
6814 for _ in 0..path_component_count {
6815 let letter = rng.lock().gen_range(b'a'..=b'z');
6816 path.push(std::str::from_utf8(&[letter]).unwrap());
6817 }
6818 path.set_extension("rs");
6819 let parent_path = path.parent().unwrap();
6820
6821 log::info!("Host: creating file {:?}", path,);
6822
6823 if fs.create_dir(parent_path).await.is_ok()
6824 && fs.create_file(&path, Default::default()).await.is_ok()
6825 {
6826 break;
6827 } else {
6828 log::info!("Host: cannot create file");
6829 }
6830 },
6831 }
6832
6833 cx.background().simulate_random_delay().await;
6834 }
6835
6836 Ok(())
6837 }
6838
6839 let result =
6840 simulate_host_internal(&mut self, project.clone(), op_start_signal, rng, &mut cx).await;
6841 log::info!("Host done");
6842 (self, project, cx, result.err())
6843 }
6844
6845 pub async fn simulate_guest(
6846 mut self,
6847 guest_username: String,
6848 project: ModelHandle<Project>,
6849 op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
6850 rng: Arc<Mutex<StdRng>>,
6851 mut cx: TestAppContext,
6852 ) -> (
6853 Self,
6854 ModelHandle<Project>,
6855 TestAppContext,
6856 Option<anyhow::Error>,
6857 ) {
6858 async fn simulate_guest_internal(
6859 client: &mut TestClient,
6860 guest_username: &str,
6861 project: ModelHandle<Project>,
6862 mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
6863 rng: Arc<Mutex<StdRng>>,
6864 cx: &mut TestAppContext,
6865 ) -> anyhow::Result<()> {
6866 while op_start_signal.next().await.is_some() {
6867 let buffer = if client.buffers.is_empty() || rng.lock().gen() {
6868 let worktree = if let Some(worktree) = project.read_with(cx, |project, cx| {
6869 project
6870 .worktrees(cx)
6871 .filter(|worktree| {
6872 let worktree = worktree.read(cx);
6873 worktree.is_visible()
6874 && worktree.entries(false).any(|e| e.is_file())
6875 })
6876 .choose(&mut *rng.lock())
6877 }) {
6878 worktree
6879 } else {
6880 cx.background().simulate_random_delay().await;
6881 continue;
6882 };
6883
6884 let (worktree_root_name, project_path) =
6885 worktree.read_with(cx, |worktree, _| {
6886 let entry = worktree
6887 .entries(false)
6888 .filter(|e| e.is_file())
6889 .choose(&mut *rng.lock())
6890 .unwrap();
6891 (
6892 worktree.root_name().to_string(),
6893 (worktree.id(), entry.path.clone()),
6894 )
6895 });
6896 log::info!(
6897 "{}: opening path {:?} in worktree {} ({})",
6898 guest_username,
6899 project_path.1,
6900 project_path.0,
6901 worktree_root_name,
6902 );
6903 let buffer = project
6904 .update(cx, |project, cx| {
6905 project.open_buffer(project_path.clone(), cx)
6906 })
6907 .await?;
6908 log::info!(
6909 "{}: opened path {:?} in worktree {} ({}) with buffer id {}",
6910 guest_username,
6911 project_path.1,
6912 project_path.0,
6913 worktree_root_name,
6914 buffer.read_with(cx, |buffer, _| buffer.remote_id())
6915 );
6916 client.buffers.insert(buffer.clone());
6917 buffer
6918 } else {
6919 client
6920 .buffers
6921 .iter()
6922 .choose(&mut *rng.lock())
6923 .unwrap()
6924 .clone()
6925 };
6926
6927 let choice = rng.lock().gen_range(0..100);
6928 match choice {
6929 0..=9 => {
6930 cx.update(|cx| {
6931 log::info!(
6932 "{}: dropping buffer {:?}",
6933 guest_username,
6934 buffer.read(cx).file().unwrap().full_path(cx)
6935 );
6936 client.buffers.remove(&buffer);
6937 drop(buffer);
6938 });
6939 }
6940 10..=19 => {
6941 let completions = project.update(cx, |project, cx| {
6942 log::info!(
6943 "{}: requesting completions for buffer {} ({:?})",
6944 guest_username,
6945 buffer.read(cx).remote_id(),
6946 buffer.read(cx).file().unwrap().full_path(cx)
6947 );
6948 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
6949 project.completions(&buffer, offset, cx)
6950 });
6951 let completions = cx.background().spawn(async move {
6952 completions
6953 .await
6954 .map_err(|err| anyhow!("completions request failed: {:?}", err))
6955 });
6956 if rng.lock().gen_bool(0.3) {
6957 log::info!("{}: detaching completions request", guest_username);
6958 cx.update(|cx| completions.detach_and_log_err(cx));
6959 } else {
6960 completions.await?;
6961 }
6962 }
6963 20..=29 => {
6964 let code_actions = project.update(cx, |project, cx| {
6965 log::info!(
6966 "{}: requesting code actions for buffer {} ({:?})",
6967 guest_username,
6968 buffer.read(cx).remote_id(),
6969 buffer.read(cx).file().unwrap().full_path(cx)
6970 );
6971 let range = buffer.read(cx).random_byte_range(0, &mut *rng.lock());
6972 project.code_actions(&buffer, range, cx)
6973 });
6974 let code_actions = cx.background().spawn(async move {
6975 code_actions
6976 .await
6977 .map_err(|err| anyhow!("code actions request failed: {:?}", err))
6978 });
6979 if rng.lock().gen_bool(0.3) {
6980 log::info!("{}: detaching code actions request", guest_username);
6981 cx.update(|cx| code_actions.detach_and_log_err(cx));
6982 } else {
6983 code_actions.await?;
6984 }
6985 }
6986 30..=39 if buffer.read_with(cx, |buffer, _| buffer.is_dirty()) => {
6987 let (requested_version, save) = buffer.update(cx, |buffer, cx| {
6988 log::info!(
6989 "{}: saving buffer {} ({:?})",
6990 guest_username,
6991 buffer.remote_id(),
6992 buffer.file().unwrap().full_path(cx)
6993 );
6994 (buffer.version(), buffer.save(cx))
6995 });
6996 let save = cx.background().spawn(async move {
6997 let (saved_version, _, _) = save
6998 .await
6999 .map_err(|err| anyhow!("save request failed: {:?}", err))?;
7000 assert!(saved_version.observed_all(&requested_version));
7001 Ok::<_, anyhow::Error>(())
7002 });
7003 if rng.lock().gen_bool(0.3) {
7004 log::info!("{}: detaching save request", guest_username);
7005 cx.update(|cx| save.detach_and_log_err(cx));
7006 } else {
7007 save.await?;
7008 }
7009 }
7010 40..=44 => {
7011 let prepare_rename = project.update(cx, |project, cx| {
7012 log::info!(
7013 "{}: preparing rename for buffer {} ({:?})",
7014 guest_username,
7015 buffer.read(cx).remote_id(),
7016 buffer.read(cx).file().unwrap().full_path(cx)
7017 );
7018 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
7019 project.prepare_rename(buffer, offset, cx)
7020 });
7021 let prepare_rename = cx.background().spawn(async move {
7022 prepare_rename
7023 .await
7024 .map_err(|err| anyhow!("prepare rename request failed: {:?}", err))
7025 });
7026 if rng.lock().gen_bool(0.3) {
7027 log::info!("{}: detaching prepare rename request", guest_username);
7028 cx.update(|cx| prepare_rename.detach_and_log_err(cx));
7029 } else {
7030 prepare_rename.await?;
7031 }
7032 }
7033 45..=49 => {
7034 let definitions = project.update(cx, |project, cx| {
7035 log::info!(
7036 "{}: requesting definitions for buffer {} ({:?})",
7037 guest_username,
7038 buffer.read(cx).remote_id(),
7039 buffer.read(cx).file().unwrap().full_path(cx)
7040 );
7041 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
7042 project.definition(&buffer, offset, cx)
7043 });
7044 let definitions = cx.background().spawn(async move {
7045 definitions
7046 .await
7047 .map_err(|err| anyhow!("definitions request failed: {:?}", err))
7048 });
7049 if rng.lock().gen_bool(0.3) {
7050 log::info!("{}: detaching definitions request", guest_username);
7051 cx.update(|cx| definitions.detach_and_log_err(cx));
7052 } else {
7053 client.buffers.extend(
7054 definitions.await?.into_iter().map(|loc| loc.target.buffer),
7055 );
7056 }
7057 }
7058 50..=54 => {
7059 let highlights = project.update(cx, |project, cx| {
7060 log::info!(
7061 "{}: requesting highlights for buffer {} ({:?})",
7062 guest_username,
7063 buffer.read(cx).remote_id(),
7064 buffer.read(cx).file().unwrap().full_path(cx)
7065 );
7066 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
7067 project.document_highlights(&buffer, offset, cx)
7068 });
7069 let highlights = cx.background().spawn(async move {
7070 highlights
7071 .await
7072 .map_err(|err| anyhow!("highlights request failed: {:?}", err))
7073 });
7074 if rng.lock().gen_bool(0.3) {
7075 log::info!("{}: detaching highlights request", guest_username);
7076 cx.update(|cx| highlights.detach_and_log_err(cx));
7077 } else {
7078 highlights.await?;
7079 }
7080 }
7081 55..=59 => {
7082 let search = project.update(cx, |project, cx| {
7083 let query = rng.lock().gen_range('a'..='z');
7084 log::info!("{}: project-wide search {:?}", guest_username, query);
7085 project.search(SearchQuery::text(query, false, false), cx)
7086 });
7087 let search = cx.background().spawn(async move {
7088 search
7089 .await
7090 .map_err(|err| anyhow!("search request failed: {:?}", err))
7091 });
7092 if rng.lock().gen_bool(0.3) {
7093 log::info!("{}: detaching search request", guest_username);
7094 cx.update(|cx| search.detach_and_log_err(cx));
7095 } else {
7096 client.buffers.extend(search.await?.into_keys());
7097 }
7098 }
7099 60..=69 => {
7100 let worktree = project
7101 .read_with(cx, |project, cx| {
7102 project
7103 .worktrees(cx)
7104 .filter(|worktree| {
7105 let worktree = worktree.read(cx);
7106 worktree.is_visible()
7107 && worktree.entries(false).any(|e| e.is_file())
7108 && worktree.root_entry().map_or(false, |e| e.is_dir())
7109 })
7110 .choose(&mut *rng.lock())
7111 })
7112 .unwrap();
7113 let (worktree_id, worktree_root_name) = worktree
7114 .read_with(cx, |worktree, _| {
7115 (worktree.id(), worktree.root_name().to_string())
7116 });
7117
7118 let mut new_name = String::new();
7119 for _ in 0..10 {
7120 let letter = rng.lock().gen_range('a'..='z');
7121 new_name.push(letter);
7122 }
7123 let mut new_path = PathBuf::new();
7124 new_path.push(new_name);
7125 new_path.set_extension("rs");
7126 log::info!(
7127 "{}: creating {:?} in worktree {} ({})",
7128 guest_username,
7129 new_path,
7130 worktree_id,
7131 worktree_root_name,
7132 );
7133 project
7134 .update(cx, |project, cx| {
7135 project.create_entry((worktree_id, new_path), false, cx)
7136 })
7137 .unwrap()
7138 .await?;
7139 }
7140 _ => {
7141 buffer.update(cx, |buffer, cx| {
7142 log::info!(
7143 "{}: updating buffer {} ({:?})",
7144 guest_username,
7145 buffer.remote_id(),
7146 buffer.file().unwrap().full_path(cx)
7147 );
7148 if rng.lock().gen_bool(0.7) {
7149 buffer.randomly_edit(&mut *rng.lock(), 5, cx);
7150 } else {
7151 buffer.randomly_undo_redo(&mut *rng.lock(), cx);
7152 }
7153 });
7154 }
7155 }
7156 cx.background().simulate_random_delay().await;
7157 }
7158 Ok(())
7159 }
7160
7161 let result = simulate_guest_internal(
7162 &mut self,
7163 &guest_username,
7164 project.clone(),
7165 op_start_signal,
7166 rng,
7167 &mut cx,
7168 )
7169 .await;
7170 log::info!("{}: done", guest_username);
7171
7172 (self, project, cx, result.err())
7173 }
7174}
7175
7176impl Drop for TestClient {
7177 fn drop(&mut self) {
7178 self.client.tear_down();
7179 }
7180}
7181
7182impl Executor for Arc<gpui::executor::Background> {
7183 type Sleep = gpui::executor::Timer;
7184
7185 fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F) {
7186 self.spawn(future).detach();
7187 }
7188
7189 fn sleep(&self, duration: Duration) -> Self::Sleep {
7190 self.as_ref().timer(duration)
7191 }
7192}
7193
7194fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> {
7195 channel
7196 .messages()
7197 .cursor::<()>()
7198 .map(|m| {
7199 (
7200 m.sender.github_login.clone(),
7201 m.body.clone(),
7202 m.is_pending(),
7203 )
7204 })
7205 .collect()
7206}
7207
7208#[derive(Debug, Eq, PartialEq)]
7209struct RoomParticipants {
7210 remote: Vec<String>,
7211 pending: Vec<String>,
7212}
7213
7214fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
7215 room.read_with(cx, |room, _| RoomParticipants {
7216 remote: room
7217 .remote_participants()
7218 .iter()
7219 .map(|(_, participant)| participant.user.github_login.clone())
7220 .collect(),
7221 pending: room
7222 .pending_participants()
7223 .iter()
7224 .map(|user| user.github_login.clone())
7225 .collect(),
7226 })
7227}