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