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