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