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