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