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