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