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