1use crate::{
2 db::{NewUserParams, ProjectId, SqliteTestDb as TestDb, UserId},
3 rpc::{Executor, Server},
4 AppState,
5};
6
7use ::rpc::Peer;
8use anyhow::anyhow;
9use call::{room, ActiveCall, ParticipantLocation, Room};
10use client::{
11 self, test::FakeHttpClient, Client, Connection, Credentials, EstablishConnectionError, PeerId,
12 User, UserStore, RECEIVE_TIMEOUT,
13};
14use collections::{BTreeMap, HashMap, HashSet};
15use editor::{
16 self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToOffset,
17 ToggleCodeActions, Undo,
18};
19use fs::{FakeFs, Fs as _, HomeDir, LineEnding};
20use futures::{channel::oneshot, Future, StreamExt as _};
21use gpui::{
22 executor::{self, Deterministic},
23 geometry::vector::vec2f,
24 test::EmptyView,
25 ModelHandle, Task, TestAppContext, ViewHandle,
26};
27use language::{
28 range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
29 LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, PointUtf16, Rope,
30};
31use live_kit_client::MacOSDisplay;
32use lsp::{self, FakeLanguageServer};
33use parking_lot::Mutex;
34use project::{
35 search::SearchQuery, DiagnosticSummary, Project, ProjectPath, ProjectStore, WorktreeId,
36};
37use rand::prelude::*;
38use serde_json::json;
39use settings::{Formatter, Settings};
40use std::{
41 cell::{Cell, RefCell},
42 env, mem,
43 ops::Deref,
44 path::{Path, PathBuf},
45 rc::Rc,
46 sync::{
47 atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
48 Arc,
49 },
50 time::Duration,
51};
52use theme::ThemeRegistry;
53use unindent::Unindent as _;
54use util::post_inc;
55use workspace::{item::Item, shared_screen::SharedScreen, SplitDirection, ToggleFollow, Workspace};
56
57#[ctor::ctor]
58fn init_logger() {
59 if std::env::var("RUST_LOG").is_ok() {
60 env_logger::init();
61 }
62}
63
64#[gpui::test(iterations = 10)]
65async fn test_basic_calls(
66 deterministic: Arc<Deterministic>,
67 cx_a: &mut TestAppContext,
68 cx_b: &mut TestAppContext,
69 cx_b2: &mut TestAppContext,
70 cx_c: &mut TestAppContext,
71) {
72 deterministic.forbid_parking();
73 let mut server = TestServer::start(cx_a.background()).await;
74
75 let start = std::time::Instant::now();
76
77 let client_a = server.create_client(cx_a, "user_a").await;
78 let client_b = server.create_client(cx_b, "user_b").await;
79 let client_c = server.create_client(cx_c, "user_c").await;
80 server
81 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
82 .await;
83
84 let active_call_a = cx_a.read(ActiveCall::global);
85 let active_call_b = cx_b.read(ActiveCall::global);
86 let active_call_c = cx_c.read(ActiveCall::global);
87
88 // Call user B from client A.
89 active_call_a
90 .update(cx_a, |call, cx| {
91 call.invite(client_b.user_id().unwrap(), None, cx)
92 })
93 .await
94 .unwrap();
95 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
96 deterministic.run_until_parked();
97 assert_eq!(
98 room_participants(&room_a, cx_a),
99 RoomParticipants {
100 remote: Default::default(),
101 pending: vec!["user_b".to_string()]
102 }
103 );
104
105 // User B receives the call.
106 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
107 let call_b = incoming_call_b.next().await.unwrap().unwrap();
108 assert_eq!(call_b.caller.github_login, "user_a");
109
110 // User B connects via another client and also receives a ring on the newly-connected client.
111 let _client_b2 = server.create_client(cx_b2, "user_b").await;
112 let active_call_b2 = cx_b2.read(ActiveCall::global);
113 let mut incoming_call_b2 = active_call_b2.read_with(cx_b2, |call, _| call.incoming());
114 deterministic.run_until_parked();
115 let call_b2 = incoming_call_b2.next().await.unwrap().unwrap();
116 assert_eq!(call_b2.caller.github_login, "user_a");
117
118 // User B joins the room using the first client.
119 active_call_b
120 .update(cx_b, |call, cx| call.accept_incoming(cx))
121 .await
122 .unwrap();
123 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
124 assert!(incoming_call_b.next().await.unwrap().is_none());
125
126 deterministic.run_until_parked();
127 assert_eq!(
128 room_participants(&room_a, cx_a),
129 RoomParticipants {
130 remote: vec!["user_b".to_string()],
131 pending: Default::default()
132 }
133 );
134 assert_eq!(
135 room_participants(&room_b, cx_b),
136 RoomParticipants {
137 remote: vec!["user_a".to_string()],
138 pending: Default::default()
139 }
140 );
141
142 // Call user C from client B.
143 let mut incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
144 active_call_b
145 .update(cx_b, |call, cx| {
146 call.invite(client_c.user_id().unwrap(), None, cx)
147 })
148 .await
149 .unwrap();
150
151 deterministic.run_until_parked();
152 assert_eq!(
153 room_participants(&room_a, cx_a),
154 RoomParticipants {
155 remote: vec!["user_b".to_string()],
156 pending: vec!["user_c".to_string()]
157 }
158 );
159 assert_eq!(
160 room_participants(&room_b, cx_b),
161 RoomParticipants {
162 remote: vec!["user_a".to_string()],
163 pending: vec!["user_c".to_string()]
164 }
165 );
166
167 // User C receives the call, but declines it.
168 let call_c = incoming_call_c.next().await.unwrap().unwrap();
169 assert_eq!(call_c.caller.github_login, "user_b");
170 active_call_c.update(cx_c, |call, _| call.decline_incoming().unwrap());
171 assert!(incoming_call_c.next().await.unwrap().is_none());
172
173 deterministic.run_until_parked();
174 assert_eq!(
175 room_participants(&room_a, cx_a),
176 RoomParticipants {
177 remote: vec!["user_b".to_string()],
178 pending: Default::default()
179 }
180 );
181 assert_eq!(
182 room_participants(&room_b, cx_b),
183 RoomParticipants {
184 remote: vec!["user_a".to_string()],
185 pending: Default::default()
186 }
187 );
188
189 // User A shares their screen
190 let display = MacOSDisplay::new();
191 let events_b = active_call_events(cx_b);
192 active_call_a
193 .update(cx_a, |call, cx| {
194 call.room().unwrap().update(cx, |room, cx| {
195 room.set_display_sources(vec![display.clone()]);
196 room.share_screen(cx)
197 })
198 })
199 .await
200 .unwrap();
201
202 deterministic.run_until_parked();
203
204 assert_eq!(events_b.borrow().len(), 1);
205 let event = events_b.borrow().first().unwrap().clone();
206 if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event {
207 assert_eq!(participant_id, client_a.peer_id().unwrap());
208 room_b.read_with(cx_b, |room, _| {
209 assert_eq!(
210 room.remote_participants()[&client_a.peer_id().unwrap()]
211 .tracks
212 .len(),
213 1
214 );
215 });
216 } else {
217 panic!("unexpected event")
218 }
219
220 // User A leaves the room.
221 active_call_a.update(cx_a, |call, cx| {
222 call.hang_up(cx).unwrap();
223 assert!(call.room().is_none());
224 });
225 deterministic.run_until_parked();
226 assert_eq!(
227 room_participants(&room_a, cx_a),
228 RoomParticipants {
229 remote: Default::default(),
230 pending: Default::default()
231 }
232 );
233 assert_eq!(
234 room_participants(&room_b, cx_b),
235 RoomParticipants {
236 remote: Default::default(),
237 pending: Default::default()
238 }
239 );
240
241 // User B gets disconnected from the LiveKit server, which causes them
242 // to automatically leave the room.
243 server
244 .test_live_kit_server
245 .disconnect_client(client_b.peer_id().unwrap().to_string())
246 .await;
247 active_call_b.update(cx_b, |call, _| assert!(call.room().is_none()));
248 assert_eq!(
249 room_participants(&room_a, cx_a),
250 RoomParticipants {
251 remote: Default::default(),
252 pending: Default::default()
253 }
254 );
255 assert_eq!(
256 room_participants(&room_b, cx_b),
257 RoomParticipants {
258 remote: Default::default(),
259 pending: Default::default()
260 }
261 );
262
263 eprintln!("finished test {:?}", start.elapsed());
264}
265
266#[gpui::test(iterations = 10)]
267async fn test_room_uniqueness(
268 deterministic: Arc<Deterministic>,
269 cx_a: &mut TestAppContext,
270 cx_a2: &mut TestAppContext,
271 cx_b: &mut TestAppContext,
272 cx_b2: &mut TestAppContext,
273 cx_c: &mut TestAppContext,
274) {
275 deterministic.forbid_parking();
276 let mut server = TestServer::start(cx_a.background()).await;
277 let client_a = server.create_client(cx_a, "user_a").await;
278 let _client_a2 = server.create_client(cx_a2, "user_a").await;
279 let client_b = server.create_client(cx_b, "user_b").await;
280 let _client_b2 = server.create_client(cx_b2, "user_b").await;
281 let client_c = server.create_client(cx_c, "user_c").await;
282 server
283 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
284 .await;
285
286 let active_call_a = cx_a.read(ActiveCall::global);
287 let active_call_a2 = cx_a2.read(ActiveCall::global);
288 let active_call_b = cx_b.read(ActiveCall::global);
289 let active_call_b2 = cx_b2.read(ActiveCall::global);
290 let active_call_c = cx_c.read(ActiveCall::global);
291
292 // Call user B from client A.
293 active_call_a
294 .update(cx_a, |call, cx| {
295 call.invite(client_b.user_id().unwrap(), None, cx)
296 })
297 .await
298 .unwrap();
299
300 // Ensure a new room can't be created given user A just created one.
301 active_call_a2
302 .update(cx_a2, |call, cx| {
303 call.invite(client_c.user_id().unwrap(), None, cx)
304 })
305 .await
306 .unwrap_err();
307 active_call_a2.read_with(cx_a2, |call, _| assert!(call.room().is_none()));
308
309 // User B receives the call from user A.
310 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
311 let call_b1 = incoming_call_b.next().await.unwrap().unwrap();
312 assert_eq!(call_b1.caller.github_login, "user_a");
313
314 // Ensure calling users A and B from client C fails.
315 active_call_c
316 .update(cx_c, |call, cx| {
317 call.invite(client_a.user_id().unwrap(), None, cx)
318 })
319 .await
320 .unwrap_err();
321 active_call_c
322 .update(cx_c, |call, cx| {
323 call.invite(client_b.user_id().unwrap(), None, cx)
324 })
325 .await
326 .unwrap_err();
327
328 // Ensure User B can't create a room while they still have an incoming call.
329 active_call_b2
330 .update(cx_b2, |call, cx| {
331 call.invite(client_c.user_id().unwrap(), None, cx)
332 })
333 .await
334 .unwrap_err();
335 active_call_b2.read_with(cx_b2, |call, _| assert!(call.room().is_none()));
336
337 // User B joins the room and calling them after they've joined still fails.
338 active_call_b
339 .update(cx_b, |call, cx| call.accept_incoming(cx))
340 .await
341 .unwrap();
342 active_call_c
343 .update(cx_c, |call, cx| {
344 call.invite(client_b.user_id().unwrap(), None, cx)
345 })
346 .await
347 .unwrap_err();
348
349 // Ensure User B can't create a room while they belong to another room.
350 active_call_b2
351 .update(cx_b2, |call, cx| {
352 call.invite(client_c.user_id().unwrap(), None, cx)
353 })
354 .await
355 .unwrap_err();
356 active_call_b2.read_with(cx_b2, |call, _| assert!(call.room().is_none()));
357
358 // Client C can successfully call client B after client B leaves the room.
359 active_call_b
360 .update(cx_b, |call, cx| call.hang_up(cx))
361 .unwrap();
362 deterministic.run_until_parked();
363 active_call_c
364 .update(cx_c, |call, cx| {
365 call.invite(client_b.user_id().unwrap(), None, cx)
366 })
367 .await
368 .unwrap();
369 deterministic.run_until_parked();
370 let call_b2 = incoming_call_b.next().await.unwrap().unwrap();
371 assert_eq!(call_b2.caller.github_login, "user_c");
372}
373
374#[gpui::test(iterations = 10)]
375async fn test_leaving_room_on_disconnection(
376 deterministic: Arc<Deterministic>,
377 cx_a: &mut TestAppContext,
378 cx_b: &mut TestAppContext,
379) {
380 deterministic.forbid_parking();
381 let mut server = TestServer::start(cx_a.background()).await;
382 let client_a = server.create_client(cx_a, "user_a").await;
383 let client_b = server.create_client(cx_b, "user_b").await;
384 server
385 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
386 .await;
387
388 let active_call_a = cx_a.read(ActiveCall::global);
389 let active_call_b = cx_b.read(ActiveCall::global);
390
391 // Call user B from client A.
392 active_call_a
393 .update(cx_a, |call, cx| {
394 call.invite(client_b.user_id().unwrap(), None, cx)
395 })
396 .await
397 .unwrap();
398 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
399
400 // User B receives the call and joins the room.
401 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
402 incoming_call_b.next().await.unwrap().unwrap();
403 active_call_b
404 .update(cx_b, |call, cx| call.accept_incoming(cx))
405 .await
406 .unwrap();
407 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
408 deterministic.run_until_parked();
409 assert_eq!(
410 room_participants(&room_a, cx_a),
411 RoomParticipants {
412 remote: vec!["user_b".to_string()],
413 pending: Default::default()
414 }
415 );
416 assert_eq!(
417 room_participants(&room_b, cx_b),
418 RoomParticipants {
419 remote: vec!["user_a".to_string()],
420 pending: Default::default()
421 }
422 );
423
424 // When user A disconnects, both client A and B clear their room on the active call.
425 server.disconnect_client(client_a.peer_id().unwrap());
426 cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
427 active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none()));
428 active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none()));
429 assert_eq!(
430 room_participants(&room_a, cx_a),
431 RoomParticipants {
432 remote: Default::default(),
433 pending: Default::default()
434 }
435 );
436 assert_eq!(
437 room_participants(&room_b, cx_b),
438 RoomParticipants {
439 remote: Default::default(),
440 pending: Default::default()
441 }
442 );
443
444 // Call user B again from client A.
445 active_call_a
446 .update(cx_a, |call, cx| {
447 call.invite(client_b.user_id().unwrap(), None, cx)
448 })
449 .await
450 .unwrap();
451 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
452
453 // User B receives the call and joins the room.
454 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
455 incoming_call_b.next().await.unwrap().unwrap();
456 active_call_b
457 .update(cx_b, |call, cx| call.accept_incoming(cx))
458 .await
459 .unwrap();
460 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
461 deterministic.run_until_parked();
462 assert_eq!(
463 room_participants(&room_a, cx_a),
464 RoomParticipants {
465 remote: vec!["user_b".to_string()],
466 pending: Default::default()
467 }
468 );
469 assert_eq!(
470 room_participants(&room_b, cx_b),
471 RoomParticipants {
472 remote: vec!["user_a".to_string()],
473 pending: Default::default()
474 }
475 );
476
477 // User B gets disconnected from the LiveKit server, which causes it
478 // to automatically leave the room.
479 server
480 .test_live_kit_server
481 .disconnect_client(client_b.peer_id().unwrap().to_string())
482 .await;
483 deterministic.run_until_parked();
484 active_call_a.update(cx_a, |call, _| assert!(call.room().is_none()));
485 active_call_b.update(cx_b, |call, _| assert!(call.room().is_none()));
486 assert_eq!(
487 room_participants(&room_a, cx_a),
488 RoomParticipants {
489 remote: Default::default(),
490 pending: Default::default()
491 }
492 );
493 assert_eq!(
494 room_participants(&room_b, cx_b),
495 RoomParticipants {
496 remote: Default::default(),
497 pending: Default::default()
498 }
499 );
500}
501
502#[gpui::test(iterations = 10)]
503async fn test_calls_on_multiple_connections(
504 deterministic: Arc<Deterministic>,
505 cx_a: &mut TestAppContext,
506 cx_b1: &mut TestAppContext,
507 cx_b2: &mut TestAppContext,
508) {
509 deterministic.forbid_parking();
510 let mut server = TestServer::start(cx_a.background()).await;
511 let client_a = server.create_client(cx_a, "user_a").await;
512 let client_b1 = server.create_client(cx_b1, "user_b").await;
513 let client_b2 = server.create_client(cx_b2, "user_b").await;
514 server
515 .make_contacts(&mut [(&client_a, cx_a), (&client_b1, cx_b1)])
516 .await;
517
518 let active_call_a = cx_a.read(ActiveCall::global);
519 let active_call_b1 = cx_b1.read(ActiveCall::global);
520 let active_call_b2 = cx_b2.read(ActiveCall::global);
521 let mut incoming_call_b1 = active_call_b1.read_with(cx_b1, |call, _| call.incoming());
522 let mut incoming_call_b2 = active_call_b2.read_with(cx_b2, |call, _| call.incoming());
523 assert!(incoming_call_b1.next().await.unwrap().is_none());
524 assert!(incoming_call_b2.next().await.unwrap().is_none());
525
526 // Call user B from client A, ensuring both clients for user B ring.
527 active_call_a
528 .update(cx_a, |call, cx| {
529 call.invite(client_b1.user_id().unwrap(), None, cx)
530 })
531 .await
532 .unwrap();
533 deterministic.run_until_parked();
534 assert!(incoming_call_b1.next().await.unwrap().is_some());
535 assert!(incoming_call_b2.next().await.unwrap().is_some());
536
537 // User B declines the call on one of the two connections, causing both connections
538 // to stop ringing.
539 active_call_b2.update(cx_b2, |call, _| call.decline_incoming().unwrap());
540 deterministic.run_until_parked();
541 assert!(incoming_call_b1.next().await.unwrap().is_none());
542 assert!(incoming_call_b2.next().await.unwrap().is_none());
543
544 // Call user B again from client A.
545 active_call_a
546 .update(cx_a, |call, cx| {
547 call.invite(client_b1.user_id().unwrap(), None, cx)
548 })
549 .await
550 .unwrap();
551 deterministic.run_until_parked();
552 assert!(incoming_call_b1.next().await.unwrap().is_some());
553 assert!(incoming_call_b2.next().await.unwrap().is_some());
554
555 // User B accepts the call on one of the two connections, causing both connections
556 // to stop ringing.
557 active_call_b2
558 .update(cx_b2, |call, cx| call.accept_incoming(cx))
559 .await
560 .unwrap();
561 deterministic.run_until_parked();
562 assert!(incoming_call_b1.next().await.unwrap().is_none());
563 assert!(incoming_call_b2.next().await.unwrap().is_none());
564
565 // User B disconnects the client that is not on the call. Everything should be fine.
566 client_b1.disconnect(&cx_b1.to_async()).unwrap();
567 deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
568 client_b1
569 .authenticate_and_connect(false, &cx_b1.to_async())
570 .await
571 .unwrap();
572
573 // User B hangs up, and user A calls them again.
574 active_call_b2.update(cx_b2, |call, cx| call.hang_up(cx).unwrap());
575 deterministic.run_until_parked();
576 active_call_a
577 .update(cx_a, |call, cx| {
578 call.invite(client_b1.user_id().unwrap(), None, cx)
579 })
580 .await
581 .unwrap();
582 deterministic.run_until_parked();
583 assert!(incoming_call_b1.next().await.unwrap().is_some());
584 assert!(incoming_call_b2.next().await.unwrap().is_some());
585
586 // User A cancels the call, causing both connections to stop ringing.
587 active_call_a
588 .update(cx_a, |call, cx| {
589 call.cancel_invite(client_b1.user_id().unwrap(), cx)
590 })
591 .await
592 .unwrap();
593 deterministic.run_until_parked();
594 assert!(incoming_call_b1.next().await.unwrap().is_none());
595 assert!(incoming_call_b2.next().await.unwrap().is_none());
596
597 // User A calls user B again.
598 active_call_a
599 .update(cx_a, |call, cx| {
600 call.invite(client_b1.user_id().unwrap(), None, cx)
601 })
602 .await
603 .unwrap();
604 deterministic.run_until_parked();
605 assert!(incoming_call_b1.next().await.unwrap().is_some());
606 assert!(incoming_call_b2.next().await.unwrap().is_some());
607
608 // User A hangs up, causing both connections to stop ringing.
609 active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
610 deterministic.run_until_parked();
611 assert!(incoming_call_b1.next().await.unwrap().is_none());
612 assert!(incoming_call_b2.next().await.unwrap().is_none());
613
614 // User A calls user B again.
615 active_call_a
616 .update(cx_a, |call, cx| {
617 call.invite(client_b1.user_id().unwrap(), None, cx)
618 })
619 .await
620 .unwrap();
621 deterministic.run_until_parked();
622 assert!(incoming_call_b1.next().await.unwrap().is_some());
623 assert!(incoming_call_b2.next().await.unwrap().is_some());
624
625 // User A disconnects, causing both connections to stop ringing.
626 server.disconnect_client(client_a.peer_id().unwrap());
627 deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
628 assert!(incoming_call_b1.next().await.unwrap().is_none());
629 assert!(incoming_call_b2.next().await.unwrap().is_none());
630
631 // User A reconnects automatically, then calls user B again.
632 active_call_a
633 .update(cx_a, |call, cx| {
634 call.invite(client_b1.user_id().unwrap(), None, cx)
635 })
636 .await
637 .unwrap();
638 deterministic.run_until_parked();
639 assert!(incoming_call_b1.next().await.unwrap().is_some());
640 assert!(incoming_call_b2.next().await.unwrap().is_some());
641
642 // User B disconnects all clients, causing user A to no longer see a pending call for them.
643 server.forbid_connections();
644 server.disconnect_client(client_b1.peer_id().unwrap());
645 server.disconnect_client(client_b2.peer_id().unwrap());
646 deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
647 active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none()));
648}
649
650#[gpui::test(iterations = 10)]
651async fn test_share_project(
652 deterministic: Arc<Deterministic>,
653 cx_a: &mut TestAppContext,
654 cx_b: &mut TestAppContext,
655 cx_c: &mut TestAppContext,
656) {
657 deterministic.forbid_parking();
658 let (_, window_b) = cx_b.add_window(|_| EmptyView);
659 let mut server = TestServer::start(cx_a.background()).await;
660 let client_a = server.create_client(cx_a, "user_a").await;
661 let client_b = server.create_client(cx_b, "user_b").await;
662 let client_c = server.create_client(cx_c, "user_c").await;
663 server
664 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
665 .await;
666 let active_call_a = cx_a.read(ActiveCall::global);
667 let active_call_b = cx_b.read(ActiveCall::global);
668 let active_call_c = cx_c.read(ActiveCall::global);
669
670 client_a
671 .fs
672 .insert_tree(
673 "/a",
674 json!({
675 ".gitignore": "ignored-dir",
676 "a.txt": "a-contents",
677 "b.txt": "b-contents",
678 "ignored-dir": {
679 "c.txt": "",
680 "d.txt": "",
681 }
682 }),
683 )
684 .await;
685
686 // Invite client B to collaborate on a project
687 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
688 active_call_a
689 .update(cx_a, |call, cx| {
690 call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
691 })
692 .await
693 .unwrap();
694
695 // Join that project as client B
696 let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
697 deterministic.run_until_parked();
698 let call = incoming_call_b.borrow().clone().unwrap();
699 assert_eq!(call.caller.github_login, "user_a");
700 let initial_project = call.initial_project.unwrap();
701 active_call_b
702 .update(cx_b, |call, cx| call.accept_incoming(cx))
703 .await
704 .unwrap();
705 let client_b_peer_id = client_b.peer_id().unwrap();
706 let project_b = client_b
707 .build_remote_project(initial_project.id, cx_b)
708 .await;
709 let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
710
711 deterministic.run_until_parked();
712 project_a.read_with(cx_a, |project, _| {
713 let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
714 assert_eq!(client_b_collaborator.replica_id, replica_id_b);
715 });
716 project_b.read_with(cx_b, |project, cx| {
717 let worktree = project.worktrees(cx).next().unwrap().read(cx);
718 assert_eq!(
719 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
720 [
721 Path::new(".gitignore"),
722 Path::new("a.txt"),
723 Path::new("b.txt"),
724 Path::new("ignored-dir"),
725 Path::new("ignored-dir/c.txt"),
726 Path::new("ignored-dir/d.txt"),
727 ]
728 );
729 });
730
731 // Open the same file as client B and client A.
732 let buffer_b = project_b
733 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
734 .await
735 .unwrap();
736 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
737 project_a.read_with(cx_a, |project, cx| {
738 assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
739 });
740 let buffer_a = project_a
741 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
742 .await
743 .unwrap();
744
745 let editor_b = cx_b.add_view(&window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
746
747 // TODO
748 // // Create a selection set as client B and see that selection set as client A.
749 // buffer_a
750 // .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1)
751 // .await;
752
753 // Edit the buffer as client B and see that edit as client A.
754 editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
755 buffer_a
756 .condition(cx_a, |buffer, _| buffer.text() == "ok, b-contents")
757 .await;
758
759 // Client B can invite client C on a project shared by client A.
760 active_call_b
761 .update(cx_b, |call, cx| {
762 call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
763 })
764 .await
765 .unwrap();
766
767 let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
768 deterministic.run_until_parked();
769 let call = incoming_call_c.borrow().clone().unwrap();
770 assert_eq!(call.caller.github_login, "user_b");
771 let initial_project = call.initial_project.unwrap();
772 active_call_c
773 .update(cx_c, |call, cx| call.accept_incoming(cx))
774 .await
775 .unwrap();
776 let _project_c = client_c
777 .build_remote_project(initial_project.id, cx_c)
778 .await;
779
780 // TODO
781 // // Remove the selection set as client B, see those selections disappear as client A.
782 cx_b.update(move |_| drop(editor_b));
783 // buffer_a
784 // .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0)
785 // .await;
786}
787
788#[gpui::test(iterations = 10)]
789async fn test_unshare_project(
790 deterministic: Arc<Deterministic>,
791 cx_a: &mut TestAppContext,
792 cx_b: &mut TestAppContext,
793 cx_c: &mut TestAppContext,
794) {
795 deterministic.forbid_parking();
796 let mut server = TestServer::start(cx_a.background()).await;
797 let client_a = server.create_client(cx_a, "user_a").await;
798 let client_b = server.create_client(cx_b, "user_b").await;
799 let client_c = server.create_client(cx_c, "user_c").await;
800 server
801 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
802 .await;
803
804 let active_call_a = cx_a.read(ActiveCall::global);
805 let active_call_b = cx_b.read(ActiveCall::global);
806
807 client_a
808 .fs
809 .insert_tree(
810 "/a",
811 json!({
812 "a.txt": "a-contents",
813 "b.txt": "b-contents",
814 }),
815 )
816 .await;
817
818 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
819 let project_id = active_call_a
820 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
821 .await
822 .unwrap();
823 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
824 let project_b = client_b.build_remote_project(project_id, cx_b).await;
825 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
826
827 project_b
828 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
829 .await
830 .unwrap();
831
832 // When client B leaves the room, the project becomes read-only.
833 active_call_b.update(cx_b, |call, cx| call.hang_up(cx).unwrap());
834 deterministic.run_until_parked();
835 assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
836
837 // Client C opens the project.
838 let project_c = client_c.build_remote_project(project_id, cx_c).await;
839
840 // When client A unshares the project, client C's project becomes read-only.
841 project_a
842 .update(cx_a, |project, cx| project.unshare(cx))
843 .unwrap();
844 deterministic.run_until_parked();
845 assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
846 assert!(project_c.read_with(cx_c, |project, _| project.is_read_only()));
847
848 // Client C can open the project again after client A re-shares.
849 let project_id = active_call_a
850 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
851 .await
852 .unwrap();
853 let project_c2 = client_c.build_remote_project(project_id, cx_c).await;
854 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
855 project_c2
856 .update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
857 .await
858 .unwrap();
859
860 // When client A (the host) leaves the room, the project gets unshared and guests are notified.
861 active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
862 deterministic.run_until_parked();
863 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
864 project_c2.read_with(cx_c, |project, _| {
865 assert!(project.is_read_only());
866 assert!(project.collaborators().is_empty());
867 });
868}
869
870#[gpui::test(iterations = 10)]
871async fn test_host_disconnect(
872 deterministic: Arc<Deterministic>,
873 cx_a: &mut TestAppContext,
874 cx_b: &mut TestAppContext,
875 cx_c: &mut TestAppContext,
876) {
877 cx_b.update(editor::init);
878 deterministic.forbid_parking();
879 let mut server = TestServer::start(cx_a.background()).await;
880 let client_a = server.create_client(cx_a, "user_a").await;
881 let client_b = server.create_client(cx_b, "user_b").await;
882 let client_c = server.create_client(cx_c, "user_c").await;
883 server
884 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
885 .await;
886
887 client_a
888 .fs
889 .insert_tree(
890 "/a",
891 json!({
892 "a.txt": "a-contents",
893 "b.txt": "b-contents",
894 }),
895 )
896 .await;
897
898 let active_call_a = cx_a.read(ActiveCall::global);
899 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
900 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
901 let project_id = active_call_a
902 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
903 .await
904 .unwrap();
905
906 let project_b = client_b.build_remote_project(project_id, cx_b).await;
907 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
908
909 let (_, workspace_b) = cx_b.add_window(|cx| {
910 Workspace::new(
911 Default::default(),
912 project_b.clone(),
913 |_, _| unimplemented!(),
914 cx,
915 )
916 });
917 let editor_b = workspace_b
918 .update(cx_b, |workspace, cx| {
919 workspace.open_path((worktree_id, "b.txt"), None, 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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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) = cx_b.add_window(|cx| {
3712 Workspace::new(
3713 Default::default(),
3714 project_b.clone(),
3715 |_, _| unimplemented!(),
3716 cx,
3717 )
3718 });
3719 let editor_b = workspace_b
3720 .update(cx_b, |workspace, cx| {
3721 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
3722 })
3723 .await
3724 .unwrap()
3725 .downcast::<Editor>()
3726 .unwrap();
3727
3728 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3729 fake_language_server
3730 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
3731 assert_eq!(
3732 params.text_document.uri,
3733 lsp::Url::from_file_path("/a/main.rs").unwrap(),
3734 );
3735 assert_eq!(params.range.start, lsp::Position::new(0, 0));
3736 assert_eq!(params.range.end, lsp::Position::new(0, 0));
3737 Ok(None)
3738 })
3739 .next()
3740 .await;
3741
3742 // Move cursor to a location that contains code actions.
3743 editor_b.update(cx_b, |editor, cx| {
3744 editor.change_selections(None, cx, |s| {
3745 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
3746 });
3747 cx.focus(&editor_b);
3748 });
3749
3750 fake_language_server
3751 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
3752 assert_eq!(
3753 params.text_document.uri,
3754 lsp::Url::from_file_path("/a/main.rs").unwrap(),
3755 );
3756 assert_eq!(params.range.start, lsp::Position::new(1, 31));
3757 assert_eq!(params.range.end, lsp::Position::new(1, 31));
3758
3759 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
3760 lsp::CodeAction {
3761 title: "Inline into all callers".to_string(),
3762 edit: Some(lsp::WorkspaceEdit {
3763 changes: Some(
3764 [
3765 (
3766 lsp::Url::from_file_path("/a/main.rs").unwrap(),
3767 vec![lsp::TextEdit::new(
3768 lsp::Range::new(
3769 lsp::Position::new(1, 22),
3770 lsp::Position::new(1, 34),
3771 ),
3772 "4".to_string(),
3773 )],
3774 ),
3775 (
3776 lsp::Url::from_file_path("/a/other.rs").unwrap(),
3777 vec![lsp::TextEdit::new(
3778 lsp::Range::new(
3779 lsp::Position::new(0, 0),
3780 lsp::Position::new(0, 27),
3781 ),
3782 "".to_string(),
3783 )],
3784 ),
3785 ]
3786 .into_iter()
3787 .collect(),
3788 ),
3789 ..Default::default()
3790 }),
3791 data: Some(json!({
3792 "codeActionParams": {
3793 "range": {
3794 "start": {"line": 1, "column": 31},
3795 "end": {"line": 1, "column": 31},
3796 }
3797 }
3798 })),
3799 ..Default::default()
3800 },
3801 )]))
3802 })
3803 .next()
3804 .await;
3805
3806 // Toggle code actions and wait for them to display.
3807 editor_b.update(cx_b, |editor, cx| {
3808 editor.toggle_code_actions(
3809 &ToggleCodeActions {
3810 deployed_from_indicator: false,
3811 },
3812 cx,
3813 );
3814 });
3815 editor_b
3816 .condition(cx_b, |editor, _| editor.context_menu_visible())
3817 .await;
3818
3819 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
3820
3821 // Confirming the code action will trigger a resolve request.
3822 let confirm_action = workspace_b
3823 .update(cx_b, |workspace, cx| {
3824 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
3825 })
3826 .unwrap();
3827 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
3828 |_, _| async move {
3829 Ok(lsp::CodeAction {
3830 title: "Inline into all callers".to_string(),
3831 edit: Some(lsp::WorkspaceEdit {
3832 changes: Some(
3833 [
3834 (
3835 lsp::Url::from_file_path("/a/main.rs").unwrap(),
3836 vec![lsp::TextEdit::new(
3837 lsp::Range::new(
3838 lsp::Position::new(1, 22),
3839 lsp::Position::new(1, 34),
3840 ),
3841 "4".to_string(),
3842 )],
3843 ),
3844 (
3845 lsp::Url::from_file_path("/a/other.rs").unwrap(),
3846 vec![lsp::TextEdit::new(
3847 lsp::Range::new(
3848 lsp::Position::new(0, 0),
3849 lsp::Position::new(0, 27),
3850 ),
3851 "".to_string(),
3852 )],
3853 ),
3854 ]
3855 .into_iter()
3856 .collect(),
3857 ),
3858 ..Default::default()
3859 }),
3860 ..Default::default()
3861 })
3862 },
3863 );
3864
3865 // After the action is confirmed, an editor containing both modified files is opened.
3866 confirm_action.await.unwrap();
3867 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
3868 workspace
3869 .active_item(cx)
3870 .unwrap()
3871 .downcast::<Editor>()
3872 .unwrap()
3873 });
3874 code_action_editor.update(cx_b, |editor, cx| {
3875 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
3876 editor.undo(&Undo, cx);
3877 assert_eq!(
3878 editor.text(cx),
3879 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
3880 );
3881 editor.redo(&Redo, cx);
3882 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
3883 });
3884}
3885
3886#[gpui::test(iterations = 10)]
3887async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3888 cx_a.foreground().forbid_parking();
3889 cx_b.update(editor::init);
3890 let mut server = TestServer::start(cx_a.background()).await;
3891 let client_a = server.create_client(cx_a, "user_a").await;
3892 let client_b = server.create_client(cx_b, "user_b").await;
3893 server
3894 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3895 .await;
3896 let active_call_a = cx_a.read(ActiveCall::global);
3897
3898 // Set up a fake language server.
3899 let mut language = Language::new(
3900 LanguageConfig {
3901 name: "Rust".into(),
3902 path_suffixes: vec!["rs".to_string()],
3903 ..Default::default()
3904 },
3905 Some(tree_sitter_rust::language()),
3906 );
3907 let mut fake_language_servers = language
3908 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3909 capabilities: lsp::ServerCapabilities {
3910 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
3911 prepare_provider: Some(true),
3912 work_done_progress_options: Default::default(),
3913 })),
3914 ..Default::default()
3915 },
3916 ..Default::default()
3917 }))
3918 .await;
3919 client_a.language_registry.add(Arc::new(language));
3920
3921 client_a
3922 .fs
3923 .insert_tree(
3924 "/dir",
3925 json!({
3926 "one.rs": "const ONE: usize = 1;",
3927 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
3928 }),
3929 )
3930 .await;
3931 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3932 let project_id = active_call_a
3933 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3934 .await
3935 .unwrap();
3936 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3937
3938 let (_window_b, workspace_b) = cx_b.add_window(|cx| {
3939 Workspace::new(
3940 Default::default(),
3941 project_b.clone(),
3942 |_, _| unimplemented!(),
3943 cx,
3944 )
3945 });
3946 let editor_b = workspace_b
3947 .update(cx_b, |workspace, cx| {
3948 workspace.open_path((worktree_id, "one.rs"), None, true, cx)
3949 })
3950 .await
3951 .unwrap()
3952 .downcast::<Editor>()
3953 .unwrap();
3954 let fake_language_server = fake_language_servers.next().await.unwrap();
3955
3956 // Move cursor to a location that can be renamed.
3957 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
3958 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
3959 editor.rename(&Rename, cx).unwrap()
3960 });
3961
3962 fake_language_server
3963 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
3964 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
3965 assert_eq!(params.position, lsp::Position::new(0, 7));
3966 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
3967 lsp::Position::new(0, 6),
3968 lsp::Position::new(0, 9),
3969 ))))
3970 })
3971 .next()
3972 .await
3973 .unwrap();
3974 prepare_rename.await.unwrap();
3975 editor_b.update(cx_b, |editor, cx| {
3976 let rename = editor.pending_rename().unwrap();
3977 let buffer = editor.buffer().read(cx).snapshot(cx);
3978 assert_eq!(
3979 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
3980 6..9
3981 );
3982 rename.editor.update(cx, |rename_editor, cx| {
3983 rename_editor.buffer().update(cx, |rename_buffer, cx| {
3984 rename_buffer.edit([(0..3, "THREE")], None, cx);
3985 });
3986 });
3987 });
3988
3989 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
3990 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
3991 });
3992 fake_language_server
3993 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
3994 assert_eq!(
3995 params.text_document_position.text_document.uri.as_str(),
3996 "file:///dir/one.rs"
3997 );
3998 assert_eq!(
3999 params.text_document_position.position,
4000 lsp::Position::new(0, 6)
4001 );
4002 assert_eq!(params.new_name, "THREE");
4003 Ok(Some(lsp::WorkspaceEdit {
4004 changes: Some(
4005 [
4006 (
4007 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
4008 vec![lsp::TextEdit::new(
4009 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4010 "THREE".to_string(),
4011 )],
4012 ),
4013 (
4014 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
4015 vec![
4016 lsp::TextEdit::new(
4017 lsp::Range::new(
4018 lsp::Position::new(0, 24),
4019 lsp::Position::new(0, 27),
4020 ),
4021 "THREE".to_string(),
4022 ),
4023 lsp::TextEdit::new(
4024 lsp::Range::new(
4025 lsp::Position::new(0, 35),
4026 lsp::Position::new(0, 38),
4027 ),
4028 "THREE".to_string(),
4029 ),
4030 ],
4031 ),
4032 ]
4033 .into_iter()
4034 .collect(),
4035 ),
4036 ..Default::default()
4037 }))
4038 })
4039 .next()
4040 .await
4041 .unwrap();
4042 confirm_rename.await.unwrap();
4043
4044 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
4045 workspace
4046 .active_item(cx)
4047 .unwrap()
4048 .downcast::<Editor>()
4049 .unwrap()
4050 });
4051 rename_editor.update(cx_b, |editor, cx| {
4052 assert_eq!(
4053 editor.text(cx),
4054 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
4055 );
4056 editor.undo(&Undo, cx);
4057 assert_eq!(
4058 editor.text(cx),
4059 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
4060 );
4061 editor.redo(&Redo, cx);
4062 assert_eq!(
4063 editor.text(cx),
4064 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
4065 );
4066 });
4067
4068 // Ensure temporary rename edits cannot be undone/redone.
4069 editor_b.update(cx_b, |editor, cx| {
4070 editor.undo(&Undo, cx);
4071 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
4072 editor.undo(&Undo, cx);
4073 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
4074 editor.redo(&Redo, cx);
4075 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
4076 })
4077}
4078
4079#[gpui::test(iterations = 10)]
4080async fn test_language_server_statuses(
4081 deterministic: Arc<Deterministic>,
4082 cx_a: &mut TestAppContext,
4083 cx_b: &mut TestAppContext,
4084) {
4085 deterministic.forbid_parking();
4086
4087 cx_b.update(editor::init);
4088 let mut server = TestServer::start(cx_a.background()).await;
4089 let client_a = server.create_client(cx_a, "user_a").await;
4090 let client_b = server.create_client(cx_b, "user_b").await;
4091 server
4092 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4093 .await;
4094 let active_call_a = cx_a.read(ActiveCall::global);
4095
4096 // Set up a fake language server.
4097 let mut language = Language::new(
4098 LanguageConfig {
4099 name: "Rust".into(),
4100 path_suffixes: vec!["rs".to_string()],
4101 ..Default::default()
4102 },
4103 Some(tree_sitter_rust::language()),
4104 );
4105 let mut fake_language_servers = language
4106 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4107 name: "the-language-server",
4108 ..Default::default()
4109 }))
4110 .await;
4111 client_a.language_registry.add(Arc::new(language));
4112
4113 client_a
4114 .fs
4115 .insert_tree(
4116 "/dir",
4117 json!({
4118 "main.rs": "const ONE: usize = 1;",
4119 }),
4120 )
4121 .await;
4122 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
4123
4124 let _buffer_a = project_a
4125 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
4126 .await
4127 .unwrap();
4128
4129 let fake_language_server = fake_language_servers.next().await.unwrap();
4130 fake_language_server.start_progress("the-token").await;
4131 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4132 token: lsp::NumberOrString::String("the-token".to_string()),
4133 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
4134 lsp::WorkDoneProgressReport {
4135 message: Some("the-message".to_string()),
4136 ..Default::default()
4137 },
4138 )),
4139 });
4140 deterministic.run_until_parked();
4141 project_a.read_with(cx_a, |project, _| {
4142 let status = project.language_server_statuses().next().unwrap();
4143 assert_eq!(status.name, "the-language-server");
4144 assert_eq!(status.pending_work.len(), 1);
4145 assert_eq!(
4146 status.pending_work["the-token"].message.as_ref().unwrap(),
4147 "the-message"
4148 );
4149 });
4150
4151 let project_id = active_call_a
4152 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4153 .await
4154 .unwrap();
4155 deterministic.run_until_parked();
4156 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4157 project_b.read_with(cx_b, |project, _| {
4158 let status = project.language_server_statuses().next().unwrap();
4159 assert_eq!(status.name, "the-language-server");
4160 });
4161
4162 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4163 token: lsp::NumberOrString::String("the-token".to_string()),
4164 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
4165 lsp::WorkDoneProgressReport {
4166 message: Some("the-message-2".to_string()),
4167 ..Default::default()
4168 },
4169 )),
4170 });
4171 deterministic.run_until_parked();
4172 project_a.read_with(cx_a, |project, _| {
4173 let status = project.language_server_statuses().next().unwrap();
4174 assert_eq!(status.name, "the-language-server");
4175 assert_eq!(status.pending_work.len(), 1);
4176 assert_eq!(
4177 status.pending_work["the-token"].message.as_ref().unwrap(),
4178 "the-message-2"
4179 );
4180 });
4181 project_b.read_with(cx_b, |project, _| {
4182 let status = project.language_server_statuses().next().unwrap();
4183 assert_eq!(status.name, "the-language-server");
4184 assert_eq!(status.pending_work.len(), 1);
4185 assert_eq!(
4186 status.pending_work["the-token"].message.as_ref().unwrap(),
4187 "the-message-2"
4188 );
4189 });
4190}
4191
4192#[gpui::test(iterations = 10)]
4193async fn test_contacts(
4194 deterministic: Arc<Deterministic>,
4195 cx_a: &mut TestAppContext,
4196 cx_b: &mut TestAppContext,
4197 cx_c: &mut TestAppContext,
4198) {
4199 cx_a.foreground().forbid_parking();
4200 let mut server = TestServer::start(cx_a.background()).await;
4201 let client_a = server.create_client(cx_a, "user_a").await;
4202 let client_b = server.create_client(cx_b, "user_b").await;
4203 let client_c = server.create_client(cx_c, "user_c").await;
4204 server
4205 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
4206 .await;
4207 let active_call_a = cx_a.read(ActiveCall::global);
4208 let active_call_b = cx_b.read(ActiveCall::global);
4209 let active_call_c = cx_c.read(ActiveCall::global);
4210
4211 deterministic.run_until_parked();
4212 assert_eq!(
4213 contacts(&client_a, cx_a),
4214 [
4215 ("user_b".to_string(), "online", "free"),
4216 ("user_c".to_string(), "online", "free")
4217 ]
4218 );
4219 assert_eq!(
4220 contacts(&client_b, cx_b),
4221 [
4222 ("user_a".to_string(), "online", "free"),
4223 ("user_c".to_string(), "online", "free")
4224 ]
4225 );
4226 assert_eq!(
4227 contacts(&client_c, cx_c),
4228 [
4229 ("user_a".to_string(), "online", "free"),
4230 ("user_b".to_string(), "online", "free")
4231 ]
4232 );
4233
4234 server.disconnect_client(client_c.peer_id().unwrap());
4235 server.forbid_connections();
4236 deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
4237 assert_eq!(
4238 contacts(&client_a, cx_a),
4239 [
4240 ("user_b".to_string(), "online", "free"),
4241 ("user_c".to_string(), "offline", "free")
4242 ]
4243 );
4244 assert_eq!(
4245 contacts(&client_b, cx_b),
4246 [
4247 ("user_a".to_string(), "online", "free"),
4248 ("user_c".to_string(), "offline", "free")
4249 ]
4250 );
4251 assert_eq!(contacts(&client_c, cx_c), []);
4252
4253 server.allow_connections();
4254 client_c
4255 .authenticate_and_connect(false, &cx_c.to_async())
4256 .await
4257 .unwrap();
4258
4259 deterministic.run_until_parked();
4260 assert_eq!(
4261 contacts(&client_a, cx_a),
4262 [
4263 ("user_b".to_string(), "online", "free"),
4264 ("user_c".to_string(), "online", "free")
4265 ]
4266 );
4267 assert_eq!(
4268 contacts(&client_b, cx_b),
4269 [
4270 ("user_a".to_string(), "online", "free"),
4271 ("user_c".to_string(), "online", "free")
4272 ]
4273 );
4274 assert_eq!(
4275 contacts(&client_c, cx_c),
4276 [
4277 ("user_a".to_string(), "online", "free"),
4278 ("user_b".to_string(), "online", "free")
4279 ]
4280 );
4281
4282 active_call_a
4283 .update(cx_a, |call, cx| {
4284 call.invite(client_b.user_id().unwrap(), None, cx)
4285 })
4286 .await
4287 .unwrap();
4288 deterministic.run_until_parked();
4289 assert_eq!(
4290 contacts(&client_a, cx_a),
4291 [
4292 ("user_b".to_string(), "online", "busy"),
4293 ("user_c".to_string(), "online", "free")
4294 ]
4295 );
4296 assert_eq!(
4297 contacts(&client_b, cx_b),
4298 [
4299 ("user_a".to_string(), "online", "busy"),
4300 ("user_c".to_string(), "online", "free")
4301 ]
4302 );
4303 assert_eq!(
4304 contacts(&client_c, cx_c),
4305 [
4306 ("user_a".to_string(), "online", "busy"),
4307 ("user_b".to_string(), "online", "busy")
4308 ]
4309 );
4310
4311 active_call_b.update(cx_b, |call, _| call.decline_incoming().unwrap());
4312 deterministic.run_until_parked();
4313 assert_eq!(
4314 contacts(&client_a, cx_a),
4315 [
4316 ("user_b".to_string(), "online", "free"),
4317 ("user_c".to_string(), "online", "free")
4318 ]
4319 );
4320 assert_eq!(
4321 contacts(&client_b, cx_b),
4322 [
4323 ("user_a".to_string(), "online", "free"),
4324 ("user_c".to_string(), "online", "free")
4325 ]
4326 );
4327 assert_eq!(
4328 contacts(&client_c, cx_c),
4329 [
4330 ("user_a".to_string(), "online", "free"),
4331 ("user_b".to_string(), "online", "free")
4332 ]
4333 );
4334
4335 active_call_c
4336 .update(cx_c, |call, cx| {
4337 call.invite(client_a.user_id().unwrap(), None, cx)
4338 })
4339 .await
4340 .unwrap();
4341 deterministic.run_until_parked();
4342 assert_eq!(
4343 contacts(&client_a, cx_a),
4344 [
4345 ("user_b".to_string(), "online", "free"),
4346 ("user_c".to_string(), "online", "busy")
4347 ]
4348 );
4349 assert_eq!(
4350 contacts(&client_b, cx_b),
4351 [
4352 ("user_a".to_string(), "online", "busy"),
4353 ("user_c".to_string(), "online", "busy")
4354 ]
4355 );
4356 assert_eq!(
4357 contacts(&client_c, cx_c),
4358 [
4359 ("user_a".to_string(), "online", "busy"),
4360 ("user_b".to_string(), "online", "free")
4361 ]
4362 );
4363
4364 active_call_a
4365 .update(cx_a, |call, cx| call.accept_incoming(cx))
4366 .await
4367 .unwrap();
4368 deterministic.run_until_parked();
4369 assert_eq!(
4370 contacts(&client_a, cx_a),
4371 [
4372 ("user_b".to_string(), "online", "free"),
4373 ("user_c".to_string(), "online", "busy")
4374 ]
4375 );
4376 assert_eq!(
4377 contacts(&client_b, cx_b),
4378 [
4379 ("user_a".to_string(), "online", "busy"),
4380 ("user_c".to_string(), "online", "busy")
4381 ]
4382 );
4383 assert_eq!(
4384 contacts(&client_c, cx_c),
4385 [
4386 ("user_a".to_string(), "online", "busy"),
4387 ("user_b".to_string(), "online", "free")
4388 ]
4389 );
4390
4391 active_call_a
4392 .update(cx_a, |call, cx| {
4393 call.invite(client_b.user_id().unwrap(), None, cx)
4394 })
4395 .await
4396 .unwrap();
4397 deterministic.run_until_parked();
4398 assert_eq!(
4399 contacts(&client_a, cx_a),
4400 [
4401 ("user_b".to_string(), "online", "busy"),
4402 ("user_c".to_string(), "online", "busy")
4403 ]
4404 );
4405 assert_eq!(
4406 contacts(&client_b, cx_b),
4407 [
4408 ("user_a".to_string(), "online", "busy"),
4409 ("user_c".to_string(), "online", "busy")
4410 ]
4411 );
4412 assert_eq!(
4413 contacts(&client_c, cx_c),
4414 [
4415 ("user_a".to_string(), "online", "busy"),
4416 ("user_b".to_string(), "online", "busy")
4417 ]
4418 );
4419
4420 active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
4421 deterministic.run_until_parked();
4422 assert_eq!(
4423 contacts(&client_a, cx_a),
4424 [
4425 ("user_b".to_string(), "online", "free"),
4426 ("user_c".to_string(), "online", "free")
4427 ]
4428 );
4429 assert_eq!(
4430 contacts(&client_b, cx_b),
4431 [
4432 ("user_a".to_string(), "online", "free"),
4433 ("user_c".to_string(), "online", "free")
4434 ]
4435 );
4436 assert_eq!(
4437 contacts(&client_c, cx_c),
4438 [
4439 ("user_a".to_string(), "online", "free"),
4440 ("user_b".to_string(), "online", "free")
4441 ]
4442 );
4443
4444 active_call_a
4445 .update(cx_a, |call, cx| {
4446 call.invite(client_b.user_id().unwrap(), None, cx)
4447 })
4448 .await
4449 .unwrap();
4450 deterministic.run_until_parked();
4451 assert_eq!(
4452 contacts(&client_a, cx_a),
4453 [
4454 ("user_b".to_string(), "online", "busy"),
4455 ("user_c".to_string(), "online", "free")
4456 ]
4457 );
4458 assert_eq!(
4459 contacts(&client_b, cx_b),
4460 [
4461 ("user_a".to_string(), "online", "busy"),
4462 ("user_c".to_string(), "online", "free")
4463 ]
4464 );
4465 assert_eq!(
4466 contacts(&client_c, cx_c),
4467 [
4468 ("user_a".to_string(), "online", "busy"),
4469 ("user_b".to_string(), "online", "busy")
4470 ]
4471 );
4472
4473 server.forbid_connections();
4474 server.disconnect_client(client_a.peer_id().unwrap());
4475 deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
4476 assert_eq!(contacts(&client_a, cx_a), []);
4477 assert_eq!(
4478 contacts(&client_b, cx_b),
4479 [
4480 ("user_a".to_string(), "offline", "free"),
4481 ("user_c".to_string(), "online", "free")
4482 ]
4483 );
4484 assert_eq!(
4485 contacts(&client_c, cx_c),
4486 [
4487 ("user_a".to_string(), "offline", "free"),
4488 ("user_b".to_string(), "online", "free")
4489 ]
4490 );
4491
4492 #[allow(clippy::type_complexity)]
4493 fn contacts(
4494 client: &TestClient,
4495 cx: &TestAppContext,
4496 ) -> Vec<(String, &'static str, &'static str)> {
4497 client.user_store.read_with(cx, |store, _| {
4498 store
4499 .contacts()
4500 .iter()
4501 .map(|contact| {
4502 (
4503 contact.user.github_login.clone(),
4504 if contact.online { "online" } else { "offline" },
4505 if contact.busy { "busy" } else { "free" },
4506 )
4507 })
4508 .collect()
4509 })
4510 }
4511}
4512
4513#[gpui::test(iterations = 10)]
4514async fn test_contact_requests(
4515 executor: Arc<Deterministic>,
4516 cx_a: &mut TestAppContext,
4517 cx_a2: &mut TestAppContext,
4518 cx_b: &mut TestAppContext,
4519 cx_b2: &mut TestAppContext,
4520 cx_c: &mut TestAppContext,
4521 cx_c2: &mut TestAppContext,
4522) {
4523 cx_a.foreground().forbid_parking();
4524
4525 // Connect to a server as 3 clients.
4526 let mut server = TestServer::start(cx_a.background()).await;
4527 let client_a = server.create_client(cx_a, "user_a").await;
4528 let client_a2 = server.create_client(cx_a2, "user_a").await;
4529 let client_b = server.create_client(cx_b, "user_b").await;
4530 let client_b2 = server.create_client(cx_b2, "user_b").await;
4531 let client_c = server.create_client(cx_c, "user_c").await;
4532 let client_c2 = server.create_client(cx_c2, "user_c").await;
4533
4534 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
4535 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
4536 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
4537
4538 // User A and User C request that user B become their contact.
4539 client_a
4540 .user_store
4541 .update(cx_a, |store, cx| {
4542 store.request_contact(client_b.user_id().unwrap(), cx)
4543 })
4544 .await
4545 .unwrap();
4546 client_c
4547 .user_store
4548 .update(cx_c, |store, cx| {
4549 store.request_contact(client_b.user_id().unwrap(), cx)
4550 })
4551 .await
4552 .unwrap();
4553 executor.run_until_parked();
4554
4555 // All users see the pending request appear in all their clients.
4556 assert_eq!(
4557 client_a.summarize_contacts(cx_a).outgoing_requests,
4558 &["user_b"]
4559 );
4560 assert_eq!(
4561 client_a2.summarize_contacts(cx_a2).outgoing_requests,
4562 &["user_b"]
4563 );
4564 assert_eq!(
4565 client_b.summarize_contacts(cx_b).incoming_requests,
4566 &["user_a", "user_c"]
4567 );
4568 assert_eq!(
4569 client_b2.summarize_contacts(cx_b2).incoming_requests,
4570 &["user_a", "user_c"]
4571 );
4572 assert_eq!(
4573 client_c.summarize_contacts(cx_c).outgoing_requests,
4574 &["user_b"]
4575 );
4576 assert_eq!(
4577 client_c2.summarize_contacts(cx_c2).outgoing_requests,
4578 &["user_b"]
4579 );
4580
4581 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
4582 disconnect_and_reconnect(&client_a, cx_a).await;
4583 disconnect_and_reconnect(&client_b, cx_b).await;
4584 disconnect_and_reconnect(&client_c, cx_c).await;
4585 executor.run_until_parked();
4586 assert_eq!(
4587 client_a.summarize_contacts(cx_a).outgoing_requests,
4588 &["user_b"]
4589 );
4590 assert_eq!(
4591 client_b.summarize_contacts(cx_b).incoming_requests,
4592 &["user_a", "user_c"]
4593 );
4594 assert_eq!(
4595 client_c.summarize_contacts(cx_c).outgoing_requests,
4596 &["user_b"]
4597 );
4598
4599 // User B accepts the request from user A.
4600 client_b
4601 .user_store
4602 .update(cx_b, |store, cx| {
4603 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
4604 })
4605 .await
4606 .unwrap();
4607
4608 executor.run_until_parked();
4609
4610 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
4611 let contacts_b = client_b.summarize_contacts(cx_b);
4612 assert_eq!(contacts_b.current, &["user_a"]);
4613 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
4614 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
4615 assert_eq!(contacts_b2.current, &["user_a"]);
4616 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
4617
4618 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
4619 let contacts_a = client_a.summarize_contacts(cx_a);
4620 assert_eq!(contacts_a.current, &["user_b"]);
4621 assert!(contacts_a.outgoing_requests.is_empty());
4622 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
4623 assert_eq!(contacts_a2.current, &["user_b"]);
4624 assert!(contacts_a2.outgoing_requests.is_empty());
4625
4626 // Contacts are present upon connecting (tested here via disconnect/reconnect)
4627 disconnect_and_reconnect(&client_a, cx_a).await;
4628 disconnect_and_reconnect(&client_b, cx_b).await;
4629 disconnect_and_reconnect(&client_c, cx_c).await;
4630 executor.run_until_parked();
4631 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
4632 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
4633 assert_eq!(
4634 client_b.summarize_contacts(cx_b).incoming_requests,
4635 &["user_c"]
4636 );
4637 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
4638 assert_eq!(
4639 client_c.summarize_contacts(cx_c).outgoing_requests,
4640 &["user_b"]
4641 );
4642
4643 // User B rejects the request from user C.
4644 client_b
4645 .user_store
4646 .update(cx_b, |store, cx| {
4647 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
4648 })
4649 .await
4650 .unwrap();
4651
4652 executor.run_until_parked();
4653
4654 // User B doesn't see user C as their contact, and the incoming request from them is removed.
4655 let contacts_b = client_b.summarize_contacts(cx_b);
4656 assert_eq!(contacts_b.current, &["user_a"]);
4657 assert!(contacts_b.incoming_requests.is_empty());
4658 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
4659 assert_eq!(contacts_b2.current, &["user_a"]);
4660 assert!(contacts_b2.incoming_requests.is_empty());
4661
4662 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
4663 let contacts_c = client_c.summarize_contacts(cx_c);
4664 assert!(contacts_c.current.is_empty());
4665 assert!(contacts_c.outgoing_requests.is_empty());
4666 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
4667 assert!(contacts_c2.current.is_empty());
4668 assert!(contacts_c2.outgoing_requests.is_empty());
4669
4670 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
4671 disconnect_and_reconnect(&client_a, cx_a).await;
4672 disconnect_and_reconnect(&client_b, cx_b).await;
4673 disconnect_and_reconnect(&client_c, cx_c).await;
4674 executor.run_until_parked();
4675 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
4676 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
4677 assert!(client_b
4678 .summarize_contacts(cx_b)
4679 .incoming_requests
4680 .is_empty());
4681 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
4682 assert!(client_c
4683 .summarize_contacts(cx_c)
4684 .outgoing_requests
4685 .is_empty());
4686
4687 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
4688 client.disconnect(&cx.to_async()).unwrap();
4689 client.clear_contacts(cx).await;
4690 client
4691 .authenticate_and_connect(false, &cx.to_async())
4692 .await
4693 .unwrap();
4694 }
4695}
4696
4697#[gpui::test(iterations = 10)]
4698async fn test_following(
4699 deterministic: Arc<Deterministic>,
4700 cx_a: &mut TestAppContext,
4701 cx_b: &mut TestAppContext,
4702) {
4703 cx_a.foreground().forbid_parking();
4704 cx_a.update(editor::init);
4705 cx_b.update(editor::init);
4706
4707 let mut server = TestServer::start(cx_a.background()).await;
4708 let client_a = server.create_client(cx_a, "user_a").await;
4709 let client_b = server.create_client(cx_b, "user_b").await;
4710 server
4711 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4712 .await;
4713 let active_call_a = cx_a.read(ActiveCall::global);
4714 let active_call_b = cx_b.read(ActiveCall::global);
4715
4716 client_a
4717 .fs
4718 .insert_tree(
4719 "/a",
4720 json!({
4721 "1.txt": "one",
4722 "2.txt": "two",
4723 "3.txt": "three",
4724 }),
4725 )
4726 .await;
4727 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4728 active_call_a
4729 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
4730 .await
4731 .unwrap();
4732
4733 let project_id = active_call_a
4734 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4735 .await
4736 .unwrap();
4737 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4738 active_call_b
4739 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
4740 .await
4741 .unwrap();
4742
4743 // Client A opens some editors.
4744 let workspace_a = client_a.build_workspace(&project_a, cx_a);
4745 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
4746 let editor_a1 = workspace_a
4747 .update(cx_a, |workspace, cx| {
4748 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
4749 })
4750 .await
4751 .unwrap()
4752 .downcast::<Editor>()
4753 .unwrap();
4754 let editor_a2 = workspace_a
4755 .update(cx_a, |workspace, cx| {
4756 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
4757 })
4758 .await
4759 .unwrap()
4760 .downcast::<Editor>()
4761 .unwrap();
4762
4763 // Client B opens an editor.
4764 let workspace_b = client_b.build_workspace(&project_b, cx_b);
4765 let editor_b1 = workspace_b
4766 .update(cx_b, |workspace, cx| {
4767 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
4768 })
4769 .await
4770 .unwrap()
4771 .downcast::<Editor>()
4772 .unwrap();
4773
4774 let client_a_id = project_b.read_with(cx_b, |project, _| {
4775 project.collaborators().values().next().unwrap().peer_id
4776 });
4777 let client_b_id = project_a.read_with(cx_a, |project, _| {
4778 project.collaborators().values().next().unwrap().peer_id
4779 });
4780
4781 // When client B starts following client A, all visible view states are replicated to client B.
4782 editor_a1.update(cx_a, |editor, cx| {
4783 editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
4784 });
4785 editor_a2.update(cx_a, |editor, cx| {
4786 editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
4787 });
4788 workspace_b
4789 .update(cx_b, |workspace, cx| {
4790 workspace
4791 .toggle_follow(&ToggleFollow(client_a_id), cx)
4792 .unwrap()
4793 })
4794 .await
4795 .unwrap();
4796
4797 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
4798 workspace
4799 .active_item(cx)
4800 .unwrap()
4801 .downcast::<Editor>()
4802 .unwrap()
4803 });
4804 assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
4805 assert_eq!(
4806 editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)),
4807 Some((worktree_id, "2.txt").into())
4808 );
4809 assert_eq!(
4810 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
4811 vec![2..3]
4812 );
4813 assert_eq!(
4814 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
4815 vec![0..1]
4816 );
4817
4818 // When client A activates a different editor, client B does so as well.
4819 workspace_a.update(cx_a, |workspace, cx| {
4820 workspace.activate_item(&editor_a1, cx)
4821 });
4822 workspace_b
4823 .condition(cx_b, |workspace, cx| {
4824 workspace.active_item(cx).unwrap().id() == editor_b1.id()
4825 })
4826 .await;
4827
4828 // When client A navigates back and forth, client B does so as well.
4829 workspace_a
4830 .update(cx_a, |workspace, cx| {
4831 workspace::Pane::go_back(workspace, None, cx)
4832 })
4833 .await;
4834 workspace_b
4835 .condition(cx_b, |workspace, cx| {
4836 workspace.active_item(cx).unwrap().id() == editor_b2.id()
4837 })
4838 .await;
4839
4840 workspace_a
4841 .update(cx_a, |workspace, cx| {
4842 workspace::Pane::go_forward(workspace, None, cx)
4843 })
4844 .await;
4845 workspace_b
4846 .condition(cx_b, |workspace, cx| {
4847 workspace.active_item(cx).unwrap().id() == editor_b1.id()
4848 })
4849 .await;
4850
4851 // Changes to client A's editor are reflected on client B.
4852 editor_a1.update(cx_a, |editor, cx| {
4853 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
4854 });
4855 editor_b1
4856 .condition(cx_b, |editor, cx| {
4857 editor.selections.ranges(cx) == vec![1..1, 2..2]
4858 })
4859 .await;
4860
4861 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
4862 editor_b1
4863 .condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
4864 .await;
4865
4866 editor_a1.update(cx_a, |editor, cx| {
4867 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
4868 editor.set_scroll_position(vec2f(0., 100.), cx);
4869 });
4870 editor_b1
4871 .condition(cx_b, |editor, cx| {
4872 editor.selections.ranges(cx) == vec![3..3]
4873 })
4874 .await;
4875
4876 // After unfollowing, client B stops receiving updates from client A.
4877 workspace_b.update(cx_b, |workspace, cx| {
4878 workspace.unfollow(&workspace.active_pane().clone(), cx)
4879 });
4880 workspace_a.update(cx_a, |workspace, cx| {
4881 workspace.activate_item(&editor_a2, cx)
4882 });
4883 deterministic.run_until_parked();
4884 assert_eq!(
4885 workspace_b.read_with(cx_b, |workspace, cx| workspace
4886 .active_item(cx)
4887 .unwrap()
4888 .id()),
4889 editor_b1.id()
4890 );
4891
4892 // Client A starts following client B.
4893 workspace_a
4894 .update(cx_a, |workspace, cx| {
4895 workspace
4896 .toggle_follow(&ToggleFollow(client_b_id), cx)
4897 .unwrap()
4898 })
4899 .await
4900 .unwrap();
4901 assert_eq!(
4902 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
4903 Some(client_b_id)
4904 );
4905 assert_eq!(
4906 workspace_a.read_with(cx_a, |workspace, cx| workspace
4907 .active_item(cx)
4908 .unwrap()
4909 .id()),
4910 editor_a1.id()
4911 );
4912
4913 // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
4914 let display = MacOSDisplay::new();
4915 active_call_b
4916 .update(cx_b, |call, cx| call.set_location(None, cx))
4917 .await
4918 .unwrap();
4919 active_call_b
4920 .update(cx_b, |call, cx| {
4921 call.room().unwrap().update(cx, |room, cx| {
4922 room.set_display_sources(vec![display.clone()]);
4923 room.share_screen(cx)
4924 })
4925 })
4926 .await
4927 .unwrap();
4928 deterministic.run_until_parked();
4929 let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
4930 workspace
4931 .active_item(cx)
4932 .unwrap()
4933 .downcast::<SharedScreen>()
4934 .unwrap()
4935 });
4936
4937 // Client B activates Zed again, which causes the previous editor to become focused again.
4938 active_call_b
4939 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
4940 .await
4941 .unwrap();
4942 deterministic.run_until_parked();
4943 assert_eq!(
4944 workspace_a.read_with(cx_a, |workspace, cx| workspace
4945 .active_item(cx)
4946 .unwrap()
4947 .id()),
4948 editor_a1.id()
4949 );
4950
4951 // Client B activates an external window again, and the previously-opened screen-sharing item
4952 // gets activated.
4953 active_call_b
4954 .update(cx_b, |call, cx| call.set_location(None, cx))
4955 .await
4956 .unwrap();
4957 deterministic.run_until_parked();
4958 assert_eq!(
4959 workspace_a.read_with(cx_a, |workspace, cx| workspace
4960 .active_item(cx)
4961 .unwrap()
4962 .id()),
4963 shared_screen.id()
4964 );
4965
4966 // Following interrupts when client B disconnects.
4967 client_b.disconnect(&cx_b.to_async()).unwrap();
4968 deterministic.run_until_parked();
4969 assert_eq!(
4970 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
4971 None
4972 );
4973}
4974
4975#[gpui::test(iterations = 10)]
4976async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4977 cx_a.foreground().forbid_parking();
4978 cx_a.update(editor::init);
4979 cx_b.update(editor::init);
4980
4981 let mut server = TestServer::start(cx_a.background()).await;
4982 let client_a = server.create_client(cx_a, "user_a").await;
4983 let client_b = server.create_client(cx_b, "user_b").await;
4984 server
4985 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4986 .await;
4987 let active_call_a = cx_a.read(ActiveCall::global);
4988 let active_call_b = cx_b.read(ActiveCall::global);
4989
4990 // Client A shares a project.
4991 client_a
4992 .fs
4993 .insert_tree(
4994 "/a",
4995 json!({
4996 "1.txt": "one",
4997 "2.txt": "two",
4998 "3.txt": "three",
4999 "4.txt": "four",
5000 }),
5001 )
5002 .await;
5003 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
5004 active_call_a
5005 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
5006 .await
5007 .unwrap();
5008 let project_id = active_call_a
5009 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5010 .await
5011 .unwrap();
5012
5013 // Client B joins the project.
5014 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5015 active_call_b
5016 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
5017 .await
5018 .unwrap();
5019
5020 // Client A opens some editors.
5021 let workspace_a = client_a.build_workspace(&project_a, cx_a);
5022 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
5023 let _editor_a1 = workspace_a
5024 .update(cx_a, |workspace, cx| {
5025 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
5026 })
5027 .await
5028 .unwrap()
5029 .downcast::<Editor>()
5030 .unwrap();
5031
5032 // Client B opens an editor.
5033 let workspace_b = client_b.build_workspace(&project_b, cx_b);
5034 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
5035 let _editor_b1 = workspace_b
5036 .update(cx_b, |workspace, cx| {
5037 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
5038 })
5039 .await
5040 .unwrap()
5041 .downcast::<Editor>()
5042 .unwrap();
5043
5044 // Clients A and B follow each other in split panes
5045 workspace_a.update(cx_a, |workspace, cx| {
5046 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
5047 let pane_a1 = pane_a1.clone();
5048 cx.defer(move |workspace, _| {
5049 assert_ne!(*workspace.active_pane(), pane_a1);
5050 });
5051 });
5052 workspace_a
5053 .update(cx_a, |workspace, cx| {
5054 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
5055 workspace
5056 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
5057 .unwrap()
5058 })
5059 .await
5060 .unwrap();
5061 workspace_b.update(cx_b, |workspace, cx| {
5062 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
5063 let pane_b1 = pane_b1.clone();
5064 cx.defer(move |workspace, _| {
5065 assert_ne!(*workspace.active_pane(), pane_b1);
5066 });
5067 });
5068 workspace_b
5069 .update(cx_b, |workspace, cx| {
5070 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
5071 workspace
5072 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
5073 .unwrap()
5074 })
5075 .await
5076 .unwrap();
5077
5078 workspace_a.update(cx_a, |workspace, cx| {
5079 workspace.activate_next_pane(cx);
5080 });
5081 // Wait for focus effects to be fully flushed
5082 workspace_a.update(cx_a, |workspace, _| {
5083 assert_eq!(*workspace.active_pane(), pane_a1);
5084 });
5085
5086 workspace_a
5087 .update(cx_a, |workspace, cx| {
5088 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
5089 })
5090 .await
5091 .unwrap();
5092 workspace_b.update(cx_b, |workspace, cx| {
5093 workspace.activate_next_pane(cx);
5094 });
5095
5096 workspace_b
5097 .update(cx_b, |workspace, cx| {
5098 assert_eq!(*workspace.active_pane(), pane_b1);
5099 workspace.open_path((worktree_id, "4.txt"), None, true, cx)
5100 })
5101 .await
5102 .unwrap();
5103 cx_a.foreground().run_until_parked();
5104
5105 // Ensure leader updates don't change the active pane of followers
5106 workspace_a.read_with(cx_a, |workspace, _| {
5107 assert_eq!(*workspace.active_pane(), pane_a1);
5108 });
5109 workspace_b.read_with(cx_b, |workspace, _| {
5110 assert_eq!(*workspace.active_pane(), pane_b1);
5111 });
5112
5113 // Ensure peers following each other doesn't cause an infinite loop.
5114 assert_eq!(
5115 workspace_a.read_with(cx_a, |workspace, cx| workspace
5116 .active_item(cx)
5117 .unwrap()
5118 .project_path(cx)),
5119 Some((worktree_id, "3.txt").into())
5120 );
5121 workspace_a.update(cx_a, |workspace, cx| {
5122 assert_eq!(
5123 workspace.active_item(cx).unwrap().project_path(cx),
5124 Some((worktree_id, "3.txt").into())
5125 );
5126 workspace.activate_next_pane(cx);
5127 });
5128
5129 workspace_a.update(cx_a, |workspace, cx| {
5130 assert_eq!(
5131 workspace.active_item(cx).unwrap().project_path(cx),
5132 Some((worktree_id, "4.txt").into())
5133 );
5134 });
5135
5136 workspace_b.update(cx_b, |workspace, cx| {
5137 assert_eq!(
5138 workspace.active_item(cx).unwrap().project_path(cx),
5139 Some((worktree_id, "4.txt").into())
5140 );
5141 workspace.activate_next_pane(cx);
5142 });
5143
5144 workspace_b.update(cx_b, |workspace, cx| {
5145 assert_eq!(
5146 workspace.active_item(cx).unwrap().project_path(cx),
5147 Some((worktree_id, "3.txt").into())
5148 );
5149 });
5150}
5151
5152#[gpui::test(iterations = 10)]
5153async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
5154 cx_a.foreground().forbid_parking();
5155 cx_a.update(editor::init);
5156 cx_b.update(editor::init);
5157
5158 // 2 clients connect to a server.
5159 let mut server = TestServer::start(cx_a.background()).await;
5160 let client_a = server.create_client(cx_a, "user_a").await;
5161 let client_b = server.create_client(cx_b, "user_b").await;
5162 server
5163 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5164 .await;
5165 let active_call_a = cx_a.read(ActiveCall::global);
5166 let active_call_b = cx_b.read(ActiveCall::global);
5167
5168 // Client A shares a project.
5169 client_a
5170 .fs
5171 .insert_tree(
5172 "/a",
5173 json!({
5174 "1.txt": "one",
5175 "2.txt": "two",
5176 "3.txt": "three",
5177 }),
5178 )
5179 .await;
5180 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
5181 active_call_a
5182 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
5183 .await
5184 .unwrap();
5185
5186 let project_id = active_call_a
5187 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5188 .await
5189 .unwrap();
5190 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5191 active_call_b
5192 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
5193 .await
5194 .unwrap();
5195
5196 // Client A opens some editors.
5197 let workspace_a = client_a.build_workspace(&project_a, cx_a);
5198 let _editor_a1 = workspace_a
5199 .update(cx_a, |workspace, cx| {
5200 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
5201 })
5202 .await
5203 .unwrap()
5204 .downcast::<Editor>()
5205 .unwrap();
5206
5207 // Client B starts following client A.
5208 let workspace_b = client_b.build_workspace(&project_b, cx_b);
5209 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
5210 let leader_id = project_b.read_with(cx_b, |project, _| {
5211 project.collaborators().values().next().unwrap().peer_id
5212 });
5213 workspace_b
5214 .update(cx_b, |workspace, cx| {
5215 workspace
5216 .toggle_follow(&ToggleFollow(leader_id), cx)
5217 .unwrap()
5218 })
5219 .await
5220 .unwrap();
5221 assert_eq!(
5222 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5223 Some(leader_id)
5224 );
5225 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
5226 workspace
5227 .active_item(cx)
5228 .unwrap()
5229 .downcast::<Editor>()
5230 .unwrap()
5231 });
5232
5233 // When client B moves, it automatically stops following client A.
5234 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
5235 assert_eq!(
5236 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5237 None
5238 );
5239
5240 workspace_b
5241 .update(cx_b, |workspace, cx| {
5242 workspace
5243 .toggle_follow(&ToggleFollow(leader_id), cx)
5244 .unwrap()
5245 })
5246 .await
5247 .unwrap();
5248 assert_eq!(
5249 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5250 Some(leader_id)
5251 );
5252
5253 // When client B edits, it automatically stops following client A.
5254 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
5255 assert_eq!(
5256 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5257 None
5258 );
5259
5260 workspace_b
5261 .update(cx_b, |workspace, cx| {
5262 workspace
5263 .toggle_follow(&ToggleFollow(leader_id), cx)
5264 .unwrap()
5265 })
5266 .await
5267 .unwrap();
5268 assert_eq!(
5269 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5270 Some(leader_id)
5271 );
5272
5273 // When client B scrolls, it automatically stops following client A.
5274 editor_b2.update(cx_b, |editor, cx| {
5275 editor.set_scroll_position(vec2f(0., 3.), cx)
5276 });
5277 assert_eq!(
5278 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5279 None
5280 );
5281
5282 workspace_b
5283 .update(cx_b, |workspace, cx| {
5284 workspace
5285 .toggle_follow(&ToggleFollow(leader_id), cx)
5286 .unwrap()
5287 })
5288 .await
5289 .unwrap();
5290 assert_eq!(
5291 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5292 Some(leader_id)
5293 );
5294
5295 // When client B activates a different pane, it continues following client A in the original pane.
5296 workspace_b.update(cx_b, |workspace, cx| {
5297 workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
5298 });
5299 assert_eq!(
5300 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5301 Some(leader_id)
5302 );
5303
5304 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
5305 assert_eq!(
5306 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5307 Some(leader_id)
5308 );
5309
5310 // When client B activates a different item in the original pane, it automatically stops following client A.
5311 workspace_b
5312 .update(cx_b, |workspace, cx| {
5313 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
5314 })
5315 .await
5316 .unwrap();
5317 assert_eq!(
5318 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5319 None
5320 );
5321}
5322
5323#[gpui::test(iterations = 10)]
5324async fn test_peers_simultaneously_following_each_other(
5325 deterministic: Arc<Deterministic>,
5326 cx_a: &mut TestAppContext,
5327 cx_b: &mut TestAppContext,
5328) {
5329 deterministic.forbid_parking();
5330 cx_a.update(editor::init);
5331 cx_b.update(editor::init);
5332
5333 let mut server = TestServer::start(cx_a.background()).await;
5334 let client_a = server.create_client(cx_a, "user_a").await;
5335 let client_b = server.create_client(cx_b, "user_b").await;
5336 server
5337 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5338 .await;
5339 let active_call_a = cx_a.read(ActiveCall::global);
5340
5341 client_a.fs.insert_tree("/a", json!({})).await;
5342 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
5343 let workspace_a = client_a.build_workspace(&project_a, cx_a);
5344 let project_id = active_call_a
5345 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5346 .await
5347 .unwrap();
5348
5349 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5350 let workspace_b = client_b.build_workspace(&project_b, cx_b);
5351
5352 deterministic.run_until_parked();
5353 let client_a_id = project_b.read_with(cx_b, |project, _| {
5354 project.collaborators().values().next().unwrap().peer_id
5355 });
5356 let client_b_id = project_a.read_with(cx_a, |project, _| {
5357 project.collaborators().values().next().unwrap().peer_id
5358 });
5359
5360 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
5361 workspace
5362 .toggle_follow(&ToggleFollow(client_b_id), cx)
5363 .unwrap()
5364 });
5365 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
5366 workspace
5367 .toggle_follow(&ToggleFollow(client_a_id), cx)
5368 .unwrap()
5369 });
5370
5371 futures::try_join!(a_follow_b, b_follow_a).unwrap();
5372 workspace_a.read_with(cx_a, |workspace, _| {
5373 assert_eq!(
5374 workspace.leader_for_pane(workspace.active_pane()),
5375 Some(client_b_id)
5376 );
5377 });
5378 workspace_b.read_with(cx_b, |workspace, _| {
5379 assert_eq!(
5380 workspace.leader_for_pane(workspace.active_pane()),
5381 Some(client_a_id)
5382 );
5383 });
5384}
5385
5386#[gpui::test(iterations = 100)]
5387async fn test_random_collaboration(
5388 cx: &mut TestAppContext,
5389 deterministic: Arc<Deterministic>,
5390 rng: StdRng,
5391) {
5392 deterministic.forbid_parking();
5393 let rng = Arc::new(Mutex::new(rng));
5394
5395 let max_peers = env::var("MAX_PEERS")
5396 .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
5397 .unwrap_or(5);
5398
5399 let max_operations = env::var("OPERATIONS")
5400 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
5401 .unwrap_or(10);
5402
5403 let mut server = TestServer::start(cx.background()).await;
5404 let db = server.app_state.db.clone();
5405
5406 let mut available_guests = Vec::new();
5407 for ix in 0..max_peers {
5408 let username = format!("guest-{}", ix + 1);
5409 let user_id = db
5410 .create_user(
5411 &format!("{username}@example.com"),
5412 false,
5413 NewUserParams {
5414 github_login: username.clone(),
5415 github_user_id: (ix + 1) as i32,
5416 invite_count: 0,
5417 },
5418 )
5419 .await
5420 .unwrap()
5421 .user_id;
5422 available_guests.push((user_id, username));
5423 }
5424
5425 for (ix, (user_id_a, _)) in available_guests.iter().enumerate() {
5426 for (user_id_b, _) in &available_guests[ix + 1..] {
5427 server
5428 .app_state
5429 .db
5430 .send_contact_request(*user_id_a, *user_id_b)
5431 .await
5432 .unwrap();
5433 server
5434 .app_state
5435 .db
5436 .respond_to_contact_request(*user_id_b, *user_id_a, true)
5437 .await
5438 .unwrap();
5439 }
5440 }
5441
5442 let mut clients = Vec::new();
5443 let mut user_ids = Vec::new();
5444 let mut peer_ids = Vec::new();
5445 let mut op_start_signals = Vec::new();
5446 let mut next_entity_id = 100000;
5447
5448 let mut operations = 0;
5449 while operations < max_operations {
5450 let distribution = rng.lock().gen_range(0..100);
5451 match distribution {
5452 0..=19 if !available_guests.is_empty() => {
5453 let guest_ix = rng.lock().gen_range(0..available_guests.len());
5454 let (_, guest_username) = available_guests.remove(guest_ix);
5455 log::info!("Adding new connection for {}", guest_username);
5456 next_entity_id += 100000;
5457 let mut guest_cx = TestAppContext::new(
5458 cx.foreground_platform(),
5459 cx.platform(),
5460 deterministic.build_foreground(next_entity_id),
5461 deterministic.build_background(),
5462 cx.font_cache(),
5463 cx.leak_detector(),
5464 next_entity_id,
5465 cx.function_name.clone(),
5466 );
5467
5468 let op_start_signal = futures::channel::mpsc::unbounded();
5469 let guest = server.create_client(&mut guest_cx, &guest_username).await;
5470 user_ids.push(guest.current_user_id(&guest_cx));
5471 peer_ids.push(guest.peer_id().unwrap());
5472 op_start_signals.push(op_start_signal.0);
5473 clients.push(guest_cx.foreground().spawn(guest.simulate(
5474 guest_username.clone(),
5475 op_start_signal.1,
5476 rng.clone(),
5477 guest_cx,
5478 )));
5479
5480 log::info!("Added connection for {}", guest_username);
5481 operations += 1;
5482 }
5483 20..=29 if clients.len() > 1 => {
5484 let guest_ix = rng.lock().gen_range(1..clients.len());
5485 log::info!("Removing guest {}", user_ids[guest_ix]);
5486 let removed_guest_id = user_ids.remove(guest_ix);
5487 let removed_peer_id = peer_ids.remove(guest_ix);
5488 let guest = clients.remove(guest_ix);
5489 op_start_signals.remove(guest_ix);
5490 server.forbid_connections();
5491 server.disconnect_client(removed_peer_id);
5492 deterministic.advance_clock(RECEIVE_TIMEOUT);
5493 deterministic.start_waiting();
5494 log::info!("Waiting for guest {} to exit...", removed_guest_id);
5495 let (guest, mut guest_cx) = guest.await;
5496 deterministic.finish_waiting();
5497 server.allow_connections();
5498
5499 for project in &guest.remote_projects {
5500 project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
5501 }
5502 for user_id in &user_ids {
5503 let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
5504 let contacts = server
5505 .store
5506 .lock()
5507 .await
5508 .build_initial_contacts_update(contacts)
5509 .contacts;
5510 for contact in contacts {
5511 if contact.online {
5512 assert_ne!(
5513 contact.user_id, removed_guest_id.0 as u64,
5514 "removed guest is still a contact of another peer"
5515 );
5516 }
5517 }
5518 }
5519
5520 log::info!("{} removed", guest.username);
5521 available_guests.push((removed_guest_id, guest.username.clone()));
5522 guest_cx.update(|cx| {
5523 cx.clear_globals();
5524 drop(guest);
5525 });
5526
5527 operations += 1;
5528 }
5529 _ if !op_start_signals.is_empty() => {
5530 while operations < max_operations && rng.lock().gen_bool(0.7) {
5531 op_start_signals
5532 .choose(&mut *rng.lock())
5533 .unwrap()
5534 .unbounded_send(())
5535 .unwrap();
5536 operations += 1;
5537 }
5538
5539 if rng.lock().gen_bool(0.8) {
5540 deterministic.run_until_parked();
5541 }
5542 }
5543 _ => {}
5544 }
5545 }
5546
5547 drop(op_start_signals);
5548 deterministic.start_waiting();
5549 let clients = futures::future::join_all(clients).await;
5550 deterministic.finish_waiting();
5551 deterministic.run_until_parked();
5552
5553 for (guest_client, guest_cx) in &clients {
5554 for guest_project in &guest_client.remote_projects {
5555 guest_project.read_with(guest_cx, |guest_project, cx| {
5556 let host_project = clients.iter().find_map(|(client, cx)| {
5557 let project = client.local_projects.iter().find(|host_project| {
5558 host_project.read_with(cx, |host_project, _| {
5559 host_project.remote_id() == guest_project.remote_id()
5560 })
5561 })?;
5562 Some((project, cx))
5563 });
5564
5565 if !guest_project.is_read_only() {
5566 if let Some((host_project, host_cx)) = host_project {
5567 let host_worktree_snapshots =
5568 host_project.read_with(host_cx, |host_project, cx| {
5569 host_project
5570 .worktrees(cx)
5571 .map(|worktree| {
5572 let worktree = worktree.read(cx);
5573 (worktree.id(), worktree.snapshot())
5574 })
5575 .collect::<BTreeMap<_, _>>()
5576 });
5577 let guest_worktree_snapshots = guest_project
5578 .worktrees(cx)
5579 .map(|worktree| {
5580 let worktree = worktree.read(cx);
5581 (worktree.id(), worktree.snapshot())
5582 })
5583 .collect::<BTreeMap<_, _>>();
5584
5585 assert_eq!(
5586 guest_worktree_snapshots.keys().collect::<Vec<_>>(),
5587 host_worktree_snapshots.keys().collect::<Vec<_>>(),
5588 "{} has different worktrees than the host",
5589 guest_client.username
5590 );
5591
5592 for (id, host_snapshot) in &host_worktree_snapshots {
5593 let guest_snapshot = &guest_worktree_snapshots[id];
5594 assert_eq!(
5595 guest_snapshot.root_name(),
5596 host_snapshot.root_name(),
5597 "{} has different root name than the host for worktree {}",
5598 guest_client.username,
5599 id
5600 );
5601 assert_eq!(
5602 guest_snapshot.abs_path(),
5603 host_snapshot.abs_path(),
5604 "{} has different abs path than the host for worktree {}",
5605 guest_client.username,
5606 id
5607 );
5608 assert_eq!(
5609 guest_snapshot.entries(false).collect::<Vec<_>>(),
5610 host_snapshot.entries(false).collect::<Vec<_>>(),
5611 "{} has different snapshot than the host for worktree {}",
5612 guest_client.username,
5613 id
5614 );
5615 assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id());
5616 }
5617 }
5618 }
5619
5620 guest_project.check_invariants(cx);
5621 });
5622 }
5623
5624 for (guest_project, guest_buffers) in &guest_client.buffers {
5625 let project_id = if guest_project.read_with(guest_cx, |project, _| {
5626 project.is_local() || project.is_read_only()
5627 }) {
5628 continue;
5629 } else {
5630 guest_project
5631 .read_with(guest_cx, |project, _| project.remote_id())
5632 .unwrap()
5633 };
5634
5635 let host_project = clients.iter().find_map(|(client, cx)| {
5636 let project = client.local_projects.iter().find(|host_project| {
5637 host_project.read_with(cx, |host_project, _| {
5638 host_project.remote_id() == Some(project_id)
5639 })
5640 })?;
5641 Some((project, cx))
5642 });
5643
5644 let (host_project, host_cx) = if let Some((host_project, host_cx)) = host_project {
5645 (host_project, host_cx)
5646 } else {
5647 continue;
5648 };
5649
5650 for guest_buffer in guest_buffers {
5651 let buffer_id = guest_buffer.read_with(guest_cx, |buffer, _| buffer.remote_id());
5652 let host_buffer = host_project.read_with(host_cx, |project, cx| {
5653 project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
5654 panic!(
5655 "host does not have buffer for guest:{}, peer:{:?}, id:{}",
5656 guest_client.username,
5657 guest_client.peer_id(),
5658 buffer_id
5659 )
5660 })
5661 });
5662 let path = host_buffer
5663 .read_with(host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
5664
5665 assert_eq!(
5666 guest_buffer.read_with(guest_cx, |buffer, _| buffer.deferred_ops_len()),
5667 0,
5668 "{}, buffer {}, path {:?} has deferred operations",
5669 guest_client.username,
5670 buffer_id,
5671 path,
5672 );
5673 assert_eq!(
5674 guest_buffer.read_with(guest_cx, |buffer, _| buffer.text()),
5675 host_buffer.read_with(host_cx, |buffer, _| buffer.text()),
5676 "{}, buffer {}, path {:?}, differs from the host's buffer",
5677 guest_client.username,
5678 buffer_id,
5679 path
5680 );
5681 }
5682 }
5683 }
5684
5685 for (client, mut cx) in clients {
5686 cx.update(|cx| {
5687 cx.clear_globals();
5688 drop(client);
5689 });
5690 }
5691}
5692
5693struct TestServer {
5694 peer: Arc<Peer>,
5695 app_state: Arc<AppState>,
5696 server: Arc<Server>,
5697 connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
5698 forbid_connections: Arc<AtomicBool>,
5699 _test_db: TestDb,
5700 test_live_kit_server: Arc<live_kit_client::TestServer>,
5701}
5702
5703impl TestServer {
5704 async fn start(background: Arc<executor::Background>) -> Self {
5705 static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
5706
5707 let test_db = TestDb::new(background.clone());
5708 let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst);
5709 let live_kit_server = live_kit_client::TestServer::create(
5710 format!("http://livekit.{}.test", live_kit_server_id),
5711 format!("devkey-{}", live_kit_server_id),
5712 format!("secret-{}", live_kit_server_id),
5713 background.clone(),
5714 )
5715 .unwrap();
5716 let app_state = Self::build_app_state(&test_db, &live_kit_server).await;
5717 let peer = Peer::new();
5718 let server = Server::new(app_state.clone());
5719 Self {
5720 peer,
5721 app_state,
5722 server,
5723 connection_killers: Default::default(),
5724 forbid_connections: Default::default(),
5725 _test_db: test_db,
5726 test_live_kit_server: live_kit_server,
5727 }
5728 }
5729
5730 async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
5731 cx.update(|cx| {
5732 cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf()));
5733
5734 let mut settings = Settings::test(cx);
5735 settings.projects_online_by_default = false;
5736 cx.set_global(settings);
5737 });
5738
5739 let http = FakeHttpClient::with_404_response();
5740 let user_id = if let Ok(Some(user)) = self
5741 .app_state
5742 .db
5743 .get_user_by_github_account(name, None)
5744 .await
5745 {
5746 user.id
5747 } else {
5748 self.app_state
5749 .db
5750 .create_user(
5751 &format!("{name}@example.com"),
5752 false,
5753 NewUserParams {
5754 github_login: name.into(),
5755 github_user_id: 0,
5756 invite_count: 0,
5757 },
5758 )
5759 .await
5760 .expect("creating user failed")
5761 .user_id
5762 };
5763 let client_name = name.to_string();
5764 let mut client = cx.read(|cx| Client::new(http.clone(), cx));
5765 let server = self.server.clone();
5766 let db = self.app_state.db.clone();
5767 let connection_killers = self.connection_killers.clone();
5768 let forbid_connections = self.forbid_connections.clone();
5769
5770 Arc::get_mut(&mut client)
5771 .unwrap()
5772 .set_id(user_id.0 as usize)
5773 .override_authenticate(move |cx| {
5774 cx.spawn(|_| async move {
5775 let access_token = "the-token".to_string();
5776 Ok(Credentials {
5777 user_id: user_id.0 as u64,
5778 access_token,
5779 })
5780 })
5781 })
5782 .override_establish_connection(move |credentials, cx| {
5783 assert_eq!(credentials.user_id, user_id.0 as u64);
5784 assert_eq!(credentials.access_token, "the-token");
5785
5786 let server = server.clone();
5787 let db = db.clone();
5788 let connection_killers = connection_killers.clone();
5789 let forbid_connections = forbid_connections.clone();
5790 let client_name = client_name.clone();
5791 cx.spawn(move |cx| async move {
5792 if forbid_connections.load(SeqCst) {
5793 Err(EstablishConnectionError::other(anyhow!(
5794 "server is forbidding connections"
5795 )))
5796 } else {
5797 let (client_conn, server_conn, killed) =
5798 Connection::in_memory(cx.background());
5799 let (connection_id_tx, connection_id_rx) = oneshot::channel();
5800 let user = db
5801 .get_user_by_id(user_id)
5802 .await
5803 .expect("retrieving user failed")
5804 .unwrap();
5805 cx.background()
5806 .spawn(server.handle_connection(
5807 server_conn,
5808 client_name,
5809 user,
5810 Some(connection_id_tx),
5811 cx.background(),
5812 ))
5813 .detach();
5814 let connection_id = connection_id_rx.await.unwrap();
5815 connection_killers
5816 .lock()
5817 .insert(PeerId(connection_id.0), killed);
5818 Ok(client_conn)
5819 }
5820 })
5821 });
5822
5823 let fs = FakeFs::new(cx.background());
5824 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
5825 let project_store = cx.add_model(|_| ProjectStore::new());
5826 let app_state = Arc::new(workspace::AppState {
5827 client: client.clone(),
5828 user_store: user_store.clone(),
5829 project_store: project_store.clone(),
5830 languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
5831 themes: ThemeRegistry::new((), cx.font_cache()),
5832 fs: fs.clone(),
5833 build_window_options: Default::default,
5834 initialize_workspace: |_, _, _| unimplemented!(),
5835 default_item_factory: |_, _| unimplemented!(),
5836 });
5837
5838 Project::init(&client);
5839 cx.update(|cx| {
5840 workspace::init(app_state.clone(), cx);
5841 call::init(client.clone(), user_store.clone(), cx);
5842 });
5843
5844 client
5845 .authenticate_and_connect(false, &cx.to_async())
5846 .await
5847 .unwrap();
5848
5849 let client = TestClient {
5850 client,
5851 username: name.to_string(),
5852 local_projects: Default::default(),
5853 remote_projects: Default::default(),
5854 next_root_dir_id: 0,
5855 user_store,
5856 project_store,
5857 fs,
5858 language_registry: Arc::new(LanguageRegistry::test()),
5859 buffers: Default::default(),
5860 };
5861 client.wait_for_current_user(cx).await;
5862 client
5863 }
5864
5865 fn disconnect_client(&self, peer_id: PeerId) {
5866 self.connection_killers
5867 .lock()
5868 .remove(&peer_id)
5869 .unwrap()
5870 .store(true, SeqCst);
5871 }
5872
5873 fn forbid_connections(&self) {
5874 self.forbid_connections.store(true, SeqCst);
5875 }
5876
5877 fn allow_connections(&self) {
5878 self.forbid_connections.store(false, SeqCst);
5879 }
5880
5881 async fn make_contacts(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
5882 for ix in 1..clients.len() {
5883 let (left, right) = clients.split_at_mut(ix);
5884 let (client_a, cx_a) = left.last_mut().unwrap();
5885 for (client_b, cx_b) in right {
5886 client_a
5887 .user_store
5888 .update(*cx_a, |store, cx| {
5889 store.request_contact(client_b.user_id().unwrap(), cx)
5890 })
5891 .await
5892 .unwrap();
5893 cx_a.foreground().run_until_parked();
5894 client_b
5895 .user_store
5896 .update(*cx_b, |store, cx| {
5897 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5898 })
5899 .await
5900 .unwrap();
5901 }
5902 }
5903 }
5904
5905 async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
5906 self.make_contacts(clients).await;
5907
5908 let (left, right) = clients.split_at_mut(1);
5909 let (_client_a, cx_a) = &mut left[0];
5910 let active_call_a = cx_a.read(ActiveCall::global);
5911
5912 for (client_b, cx_b) in right {
5913 let user_id_b = client_b.current_user_id(*cx_b).to_proto();
5914 active_call_a
5915 .update(*cx_a, |call, cx| call.invite(user_id_b, None, cx))
5916 .await
5917 .unwrap();
5918
5919 cx_b.foreground().run_until_parked();
5920 let active_call_b = cx_b.read(ActiveCall::global);
5921 active_call_b
5922 .update(*cx_b, |call, cx| call.accept_incoming(cx))
5923 .await
5924 .unwrap();
5925 }
5926 }
5927
5928 async fn build_app_state(
5929 test_db: &TestDb,
5930 fake_server: &live_kit_client::TestServer,
5931 ) -> Arc<AppState> {
5932 Arc::new(AppState {
5933 db: test_db.db().clone(),
5934 live_kit_client: Some(Arc::new(fake_server.create_api_client())),
5935 config: Default::default(),
5936 })
5937 }
5938}
5939
5940impl Deref for TestServer {
5941 type Target = Server;
5942
5943 fn deref(&self) -> &Self::Target {
5944 &self.server
5945 }
5946}
5947
5948impl Drop for TestServer {
5949 fn drop(&mut self) {
5950 self.peer.reset();
5951 self.test_live_kit_server.teardown().unwrap();
5952 }
5953}
5954
5955struct TestClient {
5956 client: Arc<Client>,
5957 username: String,
5958 local_projects: Vec<ModelHandle<Project>>,
5959 remote_projects: Vec<ModelHandle<Project>>,
5960 next_root_dir_id: usize,
5961 pub user_store: ModelHandle<UserStore>,
5962 pub project_store: ModelHandle<ProjectStore>,
5963 language_registry: Arc<LanguageRegistry>,
5964 fs: Arc<FakeFs>,
5965 buffers: HashMap<ModelHandle<Project>, HashSet<ModelHandle<language::Buffer>>>,
5966}
5967
5968impl Deref for TestClient {
5969 type Target = Arc<Client>;
5970
5971 fn deref(&self) -> &Self::Target {
5972 &self.client
5973 }
5974}
5975
5976struct ContactsSummary {
5977 pub current: Vec<String>,
5978 pub outgoing_requests: Vec<String>,
5979 pub incoming_requests: Vec<String>,
5980}
5981
5982impl TestClient {
5983 pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
5984 UserId::from_proto(
5985 self.user_store
5986 .read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
5987 )
5988 }
5989
5990 async fn wait_for_current_user(&self, cx: &TestAppContext) {
5991 let mut authed_user = self
5992 .user_store
5993 .read_with(cx, |user_store, _| user_store.watch_current_user());
5994 while authed_user.next().await.unwrap().is_none() {}
5995 }
5996
5997 async fn clear_contacts(&self, cx: &mut TestAppContext) {
5998 self.user_store
5999 .update(cx, |store, _| store.clear_contacts())
6000 .await;
6001 }
6002
6003 fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
6004 self.user_store.read_with(cx, |store, _| ContactsSummary {
6005 current: store
6006 .contacts()
6007 .iter()
6008 .map(|contact| contact.user.github_login.clone())
6009 .collect(),
6010 outgoing_requests: store
6011 .outgoing_contact_requests()
6012 .iter()
6013 .map(|user| user.github_login.clone())
6014 .collect(),
6015 incoming_requests: store
6016 .incoming_contact_requests()
6017 .iter()
6018 .map(|user| user.github_login.clone())
6019 .collect(),
6020 })
6021 }
6022
6023 async fn build_local_project(
6024 &self,
6025 root_path: impl AsRef<Path>,
6026 cx: &mut TestAppContext,
6027 ) -> (ModelHandle<Project>, WorktreeId) {
6028 let project = cx.update(|cx| {
6029 Project::local(
6030 self.client.clone(),
6031 self.user_store.clone(),
6032 self.project_store.clone(),
6033 self.language_registry.clone(),
6034 self.fs.clone(),
6035 cx,
6036 )
6037 });
6038 let (worktree, _) = project
6039 .update(cx, |p, cx| {
6040 p.find_or_create_local_worktree(root_path, true, cx)
6041 })
6042 .await
6043 .unwrap();
6044 worktree
6045 .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
6046 .await;
6047 (project, worktree.read_with(cx, |tree, _| tree.id()))
6048 }
6049
6050 async fn build_remote_project(
6051 &self,
6052 host_project_id: u64,
6053 guest_cx: &mut TestAppContext,
6054 ) -> ModelHandle<Project> {
6055 let project_b = guest_cx.spawn(|cx| {
6056 Project::remote(
6057 host_project_id,
6058 self.client.clone(),
6059 self.user_store.clone(),
6060 self.project_store.clone(),
6061 self.language_registry.clone(),
6062 FakeFs::new(cx.background()),
6063 cx,
6064 )
6065 });
6066 project_b.await.unwrap()
6067 }
6068
6069 fn build_workspace(
6070 &self,
6071 project: &ModelHandle<Project>,
6072 cx: &mut TestAppContext,
6073 ) -> ViewHandle<Workspace> {
6074 let (_, root_view) = cx.add_window(|_| EmptyView);
6075 cx.add_view(&root_view, |cx| {
6076 Workspace::new(
6077 Default::default(),
6078 project.clone(),
6079 |_, _| unimplemented!(),
6080 cx,
6081 )
6082 })
6083 }
6084
6085 pub async fn simulate(
6086 mut self,
6087 username: String,
6088 mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
6089 rng: Arc<Mutex<StdRng>>,
6090 mut cx: TestAppContext,
6091 ) -> (Self, TestAppContext) {
6092 async fn tick(
6093 client: &mut TestClient,
6094 username: &str,
6095 rng: Arc<Mutex<StdRng>>,
6096 cx: &mut TestAppContext,
6097 ) -> anyhow::Result<()> {
6098 let active_call = cx.read(ActiveCall::global);
6099 if active_call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
6100 if rng.lock().gen() {
6101 log::info!("{}: accepting incoming call", username);
6102 active_call
6103 .update(cx, |call, cx| call.accept_incoming(cx))
6104 .await?;
6105 } else {
6106 log::info!("{}: declining incoming call", username);
6107 active_call.update(cx, |call, _| call.decline_incoming())?;
6108 }
6109 } else {
6110 let available_contacts = client.user_store.read_with(cx, |user_store, _| {
6111 user_store
6112 .contacts()
6113 .iter()
6114 .filter(|contact| contact.online && !contact.busy)
6115 .cloned()
6116 .collect::<Vec<_>>()
6117 });
6118
6119 let distribution = rng.lock().gen_range(0..100);
6120 match distribution {
6121 0..=29 if !available_contacts.is_empty() => {
6122 let contact = available_contacts.choose(&mut *rng.lock()).unwrap();
6123 log::info!("{}: inviting {}", username, contact.user.github_login);
6124 active_call
6125 .update(cx, |call, cx| call.invite(contact.user.id, None, cx))
6126 .await?;
6127 }
6128 30..=39 if active_call.read_with(cx, |call, _| call.room().is_some()) => {
6129 log::info!("{}: hanging up", username);
6130 active_call.update(cx, |call, cx| call.hang_up(cx))?;
6131 }
6132 _ => {}
6133 }
6134 }
6135
6136 let remote_projects =
6137 if let Some(room) = active_call.read_with(cx, |call, _| call.room().cloned()) {
6138 room.read_with(cx, |room, _| {
6139 room.remote_participants()
6140 .values()
6141 .flat_map(|participant| participant.projects.clone())
6142 .collect::<Vec<_>>()
6143 })
6144 } else {
6145 Default::default()
6146 };
6147 let project = if remote_projects.is_empty() || rng.lock().gen() {
6148 if client.local_projects.is_empty() || rng.lock().gen() {
6149 let dir_paths = client.fs.directories().await;
6150 let local_project = if dir_paths.is_empty() || rng.lock().gen() {
6151 let root_path = format!(
6152 "/{}-root-{}",
6153 username,
6154 post_inc(&mut client.next_root_dir_id)
6155 );
6156 let root_path = Path::new(&root_path);
6157 client.fs.create_dir(root_path).await.unwrap();
6158 client
6159 .fs
6160 .create_file(&root_path.join("main.rs"), Default::default())
6161 .await
6162 .unwrap();
6163 log::info!("{}: opening local project at {:?}", username, root_path);
6164 client.build_local_project(root_path, cx).await.0
6165 } else {
6166 let root_path = dir_paths.choose(&mut *rng.lock()).unwrap();
6167 log::info!("{}: opening local project at {:?}", username, root_path);
6168 client.build_local_project(root_path, cx).await.0
6169 };
6170 client.local_projects.push(local_project.clone());
6171 local_project
6172 } else {
6173 client
6174 .local_projects
6175 .choose(&mut *rng.lock())
6176 .unwrap()
6177 .clone()
6178 }
6179 } else {
6180 if client.remote_projects.is_empty() || rng.lock().gen() {
6181 let remote_project_id = remote_projects.choose(&mut *rng.lock()).unwrap().id;
6182 let remote_project = if let Some(project) =
6183 client.remote_projects.iter().find(|project| {
6184 project.read_with(cx, |project, _| {
6185 project.remote_id() == Some(remote_project_id)
6186 })
6187 }) {
6188 project.clone()
6189 } else {
6190 log::info!("{}: opening remote project {}", username, remote_project_id);
6191 let remote_project = Project::remote(
6192 remote_project_id,
6193 client.client.clone(),
6194 client.user_store.clone(),
6195 client.project_store.clone(),
6196 client.language_registry.clone(),
6197 FakeFs::new(cx.background()),
6198 cx.to_async(),
6199 )
6200 .await?;
6201 client.remote_projects.push(remote_project.clone());
6202 remote_project
6203 };
6204
6205 remote_project
6206 } else {
6207 client
6208 .remote_projects
6209 .choose(&mut *rng.lock())
6210 .unwrap()
6211 .clone()
6212 }
6213 };
6214 if let Err(error) = active_call
6215 .update(cx, |call, cx| call.share_project(project.clone(), cx))
6216 .await
6217 {
6218 log::error!("{}: error sharing project, {:?}", username, error);
6219 }
6220
6221 let buffers = client.buffers.entry(project.clone()).or_default();
6222 let buffer = if buffers.is_empty() || rng.lock().gen() {
6223 let worktree = if let Some(worktree) = project.read_with(cx, |project, cx| {
6224 project
6225 .worktrees(cx)
6226 .filter(|worktree| {
6227 let worktree = worktree.read(cx);
6228 worktree.is_visible() && worktree.entries(false).any(|e| e.is_file())
6229 })
6230 .choose(&mut *rng.lock())
6231 }) {
6232 worktree
6233 } else {
6234 cx.background().simulate_random_delay().await;
6235 return Ok(());
6236 };
6237
6238 let (worktree_root_name, project_path) = worktree.read_with(cx, |worktree, _| {
6239 let entry = worktree
6240 .entries(false)
6241 .filter(|e| e.is_file())
6242 .choose(&mut *rng.lock())
6243 .unwrap();
6244 (
6245 worktree.root_name().to_string(),
6246 (worktree.id(), entry.path.clone()),
6247 )
6248 });
6249 log::info!(
6250 "{}: opening path {:?} in worktree {} ({})",
6251 username,
6252 project_path.1,
6253 project_path.0,
6254 worktree_root_name,
6255 );
6256 let buffer = project
6257 .update(cx, |project, cx| {
6258 project.open_buffer(project_path.clone(), cx)
6259 })
6260 .await?;
6261 log::info!(
6262 "{}: opened path {:?} in worktree {} ({}) with buffer id {}",
6263 username,
6264 project_path.1,
6265 project_path.0,
6266 worktree_root_name,
6267 buffer.read_with(cx, |buffer, _| buffer.remote_id())
6268 );
6269 buffers.insert(buffer.clone());
6270 buffer
6271 } else {
6272 buffers.iter().choose(&mut *rng.lock()).unwrap().clone()
6273 };
6274
6275 let choice = rng.lock().gen_range(0..100);
6276 match choice {
6277 0..=9 => {
6278 cx.update(|cx| {
6279 log::info!(
6280 "{}: dropping buffer {:?}",
6281 username,
6282 buffer.read(cx).file().unwrap().full_path(cx)
6283 );
6284 buffers.remove(&buffer);
6285 drop(buffer);
6286 });
6287 }
6288 10..=19 => {
6289 let completions = project.update(cx, |project, cx| {
6290 log::info!(
6291 "{}: requesting completions for buffer {} ({:?})",
6292 username,
6293 buffer.read(cx).remote_id(),
6294 buffer.read(cx).file().unwrap().full_path(cx)
6295 );
6296 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
6297 project.completions(&buffer, offset, cx)
6298 });
6299 let completions = cx.background().spawn(async move {
6300 completions
6301 .await
6302 .map_err(|err| anyhow!("completions request failed: {:?}", err))
6303 });
6304 if rng.lock().gen_bool(0.3) {
6305 log::info!("{}: detaching completions request", username);
6306 cx.update(|cx| completions.detach_and_log_err(cx));
6307 } else {
6308 completions.await?;
6309 }
6310 }
6311 20..=29 => {
6312 let code_actions = project.update(cx, |project, cx| {
6313 log::info!(
6314 "{}: requesting code actions for buffer {} ({:?})",
6315 username,
6316 buffer.read(cx).remote_id(),
6317 buffer.read(cx).file().unwrap().full_path(cx)
6318 );
6319 let range = buffer.read(cx).random_byte_range(0, &mut *rng.lock());
6320 project.code_actions(&buffer, range, cx)
6321 });
6322 let code_actions = cx.background().spawn(async move {
6323 code_actions
6324 .await
6325 .map_err(|err| anyhow!("code actions request failed: {:?}", err))
6326 });
6327 if rng.lock().gen_bool(0.3) {
6328 log::info!("{}: detaching code actions request", username);
6329 cx.update(|cx| code_actions.detach_and_log_err(cx));
6330 } else {
6331 code_actions.await?;
6332 }
6333 }
6334 30..=39 if buffer.read_with(cx, |buffer, _| buffer.is_dirty()) => {
6335 let (requested_version, save) = buffer.update(cx, |buffer, cx| {
6336 log::info!(
6337 "{}: saving buffer {} ({:?})",
6338 username,
6339 buffer.remote_id(),
6340 buffer.file().unwrap().full_path(cx)
6341 );
6342 (buffer.version(), buffer.save(cx))
6343 });
6344 let save = cx.background().spawn(async move {
6345 let (saved_version, _, _) = save
6346 .await
6347 .map_err(|err| anyhow!("save request failed: {:?}", err))?;
6348 assert!(saved_version.observed_all(&requested_version));
6349 Ok::<_, anyhow::Error>(())
6350 });
6351 if rng.lock().gen_bool(0.3) {
6352 log::info!("{}: detaching save request", username);
6353 cx.update(|cx| save.detach_and_log_err(cx));
6354 } else {
6355 save.await?;
6356 }
6357 }
6358 40..=44 => {
6359 let prepare_rename = project.update(cx, |project, cx| {
6360 log::info!(
6361 "{}: preparing rename for buffer {} ({:?})",
6362 username,
6363 buffer.read(cx).remote_id(),
6364 buffer.read(cx).file().unwrap().full_path(cx)
6365 );
6366 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
6367 project.prepare_rename(buffer, offset, cx)
6368 });
6369 let prepare_rename = cx.background().spawn(async move {
6370 prepare_rename
6371 .await
6372 .map_err(|err| anyhow!("prepare rename request failed: {:?}", err))
6373 });
6374 if rng.lock().gen_bool(0.3) {
6375 log::info!("{}: detaching prepare rename request", username);
6376 cx.update(|cx| prepare_rename.detach_and_log_err(cx));
6377 } else {
6378 prepare_rename.await?;
6379 }
6380 }
6381 45..=49 => {
6382 let definitions = project.update(cx, |project, cx| {
6383 log::info!(
6384 "{}: requesting definitions for buffer {} ({:?})",
6385 username,
6386 buffer.read(cx).remote_id(),
6387 buffer.read(cx).file().unwrap().full_path(cx)
6388 );
6389 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
6390 project.definition(&buffer, offset, cx)
6391 });
6392 let definitions = cx.background().spawn(async move {
6393 definitions
6394 .await
6395 .map_err(|err| anyhow!("definitions request failed: {:?}", err))
6396 });
6397 if rng.lock().gen_bool(0.3) {
6398 log::info!("{}: detaching definitions request", username);
6399 cx.update(|cx| definitions.detach_and_log_err(cx));
6400 } else {
6401 buffers.extend(definitions.await?.into_iter().map(|loc| loc.target.buffer));
6402 }
6403 }
6404 50..=54 => {
6405 let highlights = project.update(cx, |project, cx| {
6406 log::info!(
6407 "{}: requesting highlights for buffer {} ({:?})",
6408 username,
6409 buffer.read(cx).remote_id(),
6410 buffer.read(cx).file().unwrap().full_path(cx)
6411 );
6412 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
6413 project.document_highlights(&buffer, offset, cx)
6414 });
6415 let highlights = cx.background().spawn(async move {
6416 highlights
6417 .await
6418 .map_err(|err| anyhow!("highlights request failed: {:?}", err))
6419 });
6420 if rng.lock().gen_bool(0.3) {
6421 log::info!("{}: detaching highlights request", username);
6422 cx.update(|cx| highlights.detach_and_log_err(cx));
6423 } else {
6424 highlights.await?;
6425 }
6426 }
6427 55..=59 => {
6428 let search = project.update(cx, |project, cx| {
6429 let query = rng.lock().gen_range('a'..='z');
6430 log::info!("{}: project-wide search {:?}", username, query);
6431 project.search(SearchQuery::text(query, false, false), cx)
6432 });
6433 let search = cx.background().spawn(async move {
6434 search
6435 .await
6436 .map_err(|err| anyhow!("search request failed: {:?}", err))
6437 });
6438 if rng.lock().gen_bool(0.3) {
6439 log::info!("{}: detaching search request", username);
6440 cx.update(|cx| search.detach_and_log_err(cx));
6441 } else {
6442 buffers.extend(search.await?.into_keys());
6443 }
6444 }
6445 60..=69 => {
6446 let worktree = project
6447 .read_with(cx, |project, cx| {
6448 project
6449 .worktrees(cx)
6450 .filter(|worktree| {
6451 let worktree = worktree.read(cx);
6452 worktree.is_visible()
6453 && worktree.entries(false).any(|e| e.is_file())
6454 && worktree.root_entry().map_or(false, |e| e.is_dir())
6455 })
6456 .choose(&mut *rng.lock())
6457 })
6458 .unwrap();
6459 let (worktree_id, worktree_root_name) = worktree
6460 .read_with(cx, |worktree, _| {
6461 (worktree.id(), worktree.root_name().to_string())
6462 });
6463
6464 let mut new_name = String::new();
6465 for _ in 0..10 {
6466 let letter = rng.lock().gen_range('a'..='z');
6467 new_name.push(letter);
6468 }
6469
6470 let is_dir = rng.lock().gen::<bool>();
6471 let mut new_path = PathBuf::new();
6472 new_path.push(new_name);
6473 if !is_dir {
6474 new_path.set_extension("rs");
6475 }
6476 log::info!(
6477 "{}: creating {:?} in worktree {} ({})",
6478 username,
6479 new_path,
6480 worktree_id,
6481 worktree_root_name,
6482 );
6483 project
6484 .update(cx, |project, cx| {
6485 project.create_entry((worktree_id, new_path), is_dir, cx)
6486 })
6487 .unwrap()
6488 .await?;
6489 }
6490 _ => {
6491 buffer.update(cx, |buffer, cx| {
6492 log::info!(
6493 "{}: updating buffer {} ({:?})",
6494 username,
6495 buffer.remote_id(),
6496 buffer.file().unwrap().full_path(cx)
6497 );
6498 if rng.lock().gen_bool(0.7) {
6499 buffer.randomly_edit(&mut *rng.lock(), 5, cx);
6500 } else {
6501 buffer.randomly_undo_redo(&mut *rng.lock(), cx);
6502 }
6503 });
6504 }
6505 }
6506
6507 Ok(())
6508 }
6509
6510 // Setup language server
6511 let mut language = Language::new(
6512 LanguageConfig {
6513 name: "Rust".into(),
6514 path_suffixes: vec!["rs".to_string()],
6515 ..Default::default()
6516 },
6517 None,
6518 );
6519 let _fake_language_servers = language
6520 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
6521 name: "the-fake-language-server",
6522 capabilities: lsp::LanguageServer::full_capabilities(),
6523 initializer: Some(Box::new({
6524 let rng = rng.clone();
6525 let fs = self.fs.clone();
6526 move |fake_server: &mut FakeLanguageServer| {
6527 fake_server.handle_request::<lsp::request::Completion, _, _>(
6528 |_, _| async move {
6529 Ok(Some(lsp::CompletionResponse::Array(vec![
6530 lsp::CompletionItem {
6531 text_edit: Some(lsp::CompletionTextEdit::Edit(
6532 lsp::TextEdit {
6533 range: lsp::Range::new(
6534 lsp::Position::new(0, 0),
6535 lsp::Position::new(0, 0),
6536 ),
6537 new_text: "the-new-text".to_string(),
6538 },
6539 )),
6540 ..Default::default()
6541 },
6542 ])))
6543 },
6544 );
6545
6546 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
6547 |_, _| async move {
6548 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
6549 lsp::CodeAction {
6550 title: "the-code-action".to_string(),
6551 ..Default::default()
6552 },
6553 )]))
6554 },
6555 );
6556
6557 fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
6558 |params, _| async move {
6559 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
6560 params.position,
6561 params.position,
6562 ))))
6563 },
6564 );
6565
6566 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
6567 let fs = fs.clone();
6568 let rng = rng.clone();
6569 move |_, _| {
6570 let fs = fs.clone();
6571 let rng = rng.clone();
6572 async move {
6573 let files = fs.files().await;
6574 let mut rng = rng.lock();
6575 let count = rng.gen_range::<usize, _>(1..3);
6576 let files = (0..count)
6577 .map(|_| files.choose(&mut *rng).unwrap())
6578 .collect::<Vec<_>>();
6579 log::info!("LSP: Returning definitions in files {:?}", &files);
6580 Ok(Some(lsp::GotoDefinitionResponse::Array(
6581 files
6582 .into_iter()
6583 .map(|file| lsp::Location {
6584 uri: lsp::Url::from_file_path(file).unwrap(),
6585 range: Default::default(),
6586 })
6587 .collect(),
6588 )))
6589 }
6590 }
6591 });
6592
6593 fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
6594 {
6595 let rng = rng.clone();
6596 move |_, _| {
6597 let mut highlights = Vec::new();
6598 let highlight_count = rng.lock().gen_range(1..=5);
6599 for _ in 0..highlight_count {
6600 let start_row = rng.lock().gen_range(0..100);
6601 let start_column = rng.lock().gen_range(0..100);
6602 let start = PointUtf16::new(start_row, start_column);
6603 let end_row = rng.lock().gen_range(0..100);
6604 let end_column = rng.lock().gen_range(0..100);
6605 let end = PointUtf16::new(end_row, end_column);
6606 let range =
6607 if start > end { end..start } else { start..end };
6608 highlights.push(lsp::DocumentHighlight {
6609 range: range_to_lsp(range.clone()),
6610 kind: Some(lsp::DocumentHighlightKind::READ),
6611 });
6612 }
6613 highlights.sort_unstable_by_key(|highlight| {
6614 (highlight.range.start, highlight.range.end)
6615 });
6616 async move { Ok(Some(highlights)) }
6617 }
6618 },
6619 );
6620 }
6621 })),
6622 ..Default::default()
6623 }))
6624 .await;
6625 self.language_registry.add(Arc::new(language));
6626
6627 while op_start_signal.next().await.is_some() {
6628 if let Err(error) = tick(&mut self, &username, rng.clone(), &mut cx).await {
6629 log::error!("{} error: {:?}", username, error);
6630 }
6631
6632 cx.background().simulate_random_delay().await;
6633 }
6634 log::info!("{}: done", username);
6635
6636 (self, cx)
6637 }
6638}
6639
6640impl Drop for TestClient {
6641 fn drop(&mut self) {
6642 self.client.tear_down();
6643 }
6644}
6645
6646impl Executor for Arc<gpui::executor::Background> {
6647 type Sleep = gpui::executor::Timer;
6648
6649 fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F) {
6650 self.spawn(future).detach();
6651 }
6652
6653 fn sleep(&self, duration: Duration) -> Self::Sleep {
6654 self.as_ref().timer(duration)
6655 }
6656}
6657
6658#[derive(Debug, Eq, PartialEq)]
6659struct RoomParticipants {
6660 remote: Vec<String>,
6661 pending: Vec<String>,
6662}
6663
6664fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
6665 room.read_with(cx, |room, _| RoomParticipants {
6666 remote: room
6667 .remote_participants()
6668 .iter()
6669 .map(|(_, participant)| participant.user.github_login.clone())
6670 .collect(),
6671 pending: room
6672 .pending_participants()
6673 .iter()
6674 .map(|user| user.github_login.clone())
6675 .collect(),
6676 })
6677}