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