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