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