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