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