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