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