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