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