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