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