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