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