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