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