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