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