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