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