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