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