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