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(),
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)]
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 user_store = user_store.read(cx);
558 for contact in user_store.contacts() {
559 if contact.user.id == user_store.current_user().unwrap().id {
560 for project in &contact.projects {
561 let store_contains_project = project_store
562 .read(cx)
563 .projects(cx)
564 .filter_map(|project| project.read(cx).remote_id())
565 .any(|x| x == project.id);
566
567 if !store_contains_project {
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().is_empty())
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().is_empty())
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().is_empty())
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 .collect::<Vec<_>>(),
1643 &[
1644 DiagnosticEntry {
1645 range: Point::new(0, 4)..Point::new(0, 7),
1646 diagnostic: Diagnostic {
1647 group_id: 1,
1648 message: "message 1".to_string(),
1649 severity: lsp::DiagnosticSeverity::ERROR,
1650 is_primary: true,
1651 ..Default::default()
1652 }
1653 },
1654 DiagnosticEntry {
1655 range: Point::new(0, 10)..Point::new(0, 13),
1656 diagnostic: Diagnostic {
1657 group_id: 2,
1658 severity: lsp::DiagnosticSeverity::WARNING,
1659 message: "message 2".to_string(),
1660 is_primary: true,
1661 ..Default::default()
1662 }
1663 }
1664 ]
1665 );
1666 });
1667
1668 // Simulate a language server reporting no errors for a file.
1669 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
1670 lsp::PublishDiagnosticsParams {
1671 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
1672 version: None,
1673 diagnostics: vec![],
1674 },
1675 );
1676 deterministic.run_until_parked();
1677 project_a.read_with(cx_a, |project, cx| {
1678 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
1679 });
1680 project_b.read_with(cx_b, |project, cx| {
1681 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
1682 });
1683 project_c.read_with(cx_c, |project, cx| {
1684 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
1685 });
1686}
1687
1688#[gpui::test(iterations = 10)]
1689async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1690 cx_a.foreground().forbid_parking();
1691 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1692 let client_a = server.create_client(cx_a, "user_a").await;
1693 let client_b = server.create_client(cx_b, "user_b").await;
1694 server
1695 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1696 .await;
1697
1698 // Set up a fake language server.
1699 let mut language = Language::new(
1700 LanguageConfig {
1701 name: "Rust".into(),
1702 path_suffixes: vec!["rs".to_string()],
1703 ..Default::default()
1704 },
1705 Some(tree_sitter_rust::language()),
1706 );
1707 let mut fake_language_servers = language
1708 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1709 capabilities: lsp::ServerCapabilities {
1710 completion_provider: Some(lsp::CompletionOptions {
1711 trigger_characters: Some(vec![".".to_string()]),
1712 ..Default::default()
1713 }),
1714 ..Default::default()
1715 },
1716 ..Default::default()
1717 }))
1718 .await;
1719 client_a.language_registry.add(Arc::new(language));
1720
1721 client_a
1722 .fs
1723 .insert_tree(
1724 "/a",
1725 json!({
1726 "main.rs": "fn main() { a }",
1727 "other.rs": "",
1728 }),
1729 )
1730 .await;
1731 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1732 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1733
1734 // Open a file in an editor as the guest.
1735 let buffer_b = project_b
1736 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1737 .await
1738 .unwrap();
1739 let (_, window_b) = cx_b.add_window(|_| EmptyView);
1740 let editor_b = cx_b.add_view(&window_b, |cx| {
1741 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
1742 });
1743
1744 let fake_language_server = fake_language_servers.next().await.unwrap();
1745 buffer_b
1746 .condition(cx_b, |buffer, _| !buffer.completion_triggers().is_empty())
1747 .await;
1748
1749 // Type a completion trigger character as the guest.
1750 editor_b.update(cx_b, |editor, cx| {
1751 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1752 editor.handle_input(".", cx);
1753 cx.focus(&editor_b);
1754 });
1755
1756 // Receive a completion request as the host's language server.
1757 // Return some completions from the host's language server.
1758 cx_a.foreground().start_waiting();
1759 fake_language_server
1760 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
1761 assert_eq!(
1762 params.text_document_position.text_document.uri,
1763 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1764 );
1765 assert_eq!(
1766 params.text_document_position.position,
1767 lsp::Position::new(0, 14),
1768 );
1769
1770 Ok(Some(lsp::CompletionResponse::Array(vec![
1771 lsp::CompletionItem {
1772 label: "first_method(…)".into(),
1773 detail: Some("fn(&mut self, B) -> C".into()),
1774 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1775 new_text: "first_method($1)".to_string(),
1776 range: lsp::Range::new(
1777 lsp::Position::new(0, 14),
1778 lsp::Position::new(0, 14),
1779 ),
1780 })),
1781 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1782 ..Default::default()
1783 },
1784 lsp::CompletionItem {
1785 label: "second_method(…)".into(),
1786 detail: Some("fn(&mut self, C) -> D<E>".into()),
1787 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1788 new_text: "second_method()".to_string(),
1789 range: lsp::Range::new(
1790 lsp::Position::new(0, 14),
1791 lsp::Position::new(0, 14),
1792 ),
1793 })),
1794 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1795 ..Default::default()
1796 },
1797 ])))
1798 })
1799 .next()
1800 .await
1801 .unwrap();
1802 cx_a.foreground().finish_waiting();
1803
1804 // Open the buffer on the host.
1805 let buffer_a = project_a
1806 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1807 .await
1808 .unwrap();
1809 buffer_a
1810 .condition(cx_a, |buffer, _| buffer.text() == "fn main() { a. }")
1811 .await;
1812
1813 // Confirm a completion on the guest.
1814 editor_b
1815 .condition(cx_b, |editor, _| editor.context_menu_visible())
1816 .await;
1817 editor_b.update(cx_b, |editor, cx| {
1818 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
1819 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
1820 });
1821
1822 // Return a resolved completion from the host's language server.
1823 // The resolved completion has an additional text edit.
1824 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
1825 |params, _| async move {
1826 assert_eq!(params.label, "first_method(…)");
1827 Ok(lsp::CompletionItem {
1828 label: "first_method(…)".into(),
1829 detail: Some("fn(&mut self, B) -> C".into()),
1830 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1831 new_text: "first_method($1)".to_string(),
1832 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1833 })),
1834 additional_text_edits: Some(vec![lsp::TextEdit {
1835 new_text: "use d::SomeTrait;\n".to_string(),
1836 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
1837 }]),
1838 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1839 ..Default::default()
1840 })
1841 },
1842 );
1843
1844 // The additional edit is applied.
1845 buffer_a
1846 .condition(cx_a, |buffer, _| {
1847 buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
1848 })
1849 .await;
1850 buffer_b
1851 .condition(cx_b, |buffer, _| {
1852 buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
1853 })
1854 .await;
1855}
1856
1857#[gpui::test(iterations = 10)]
1858async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1859 cx_a.foreground().forbid_parking();
1860 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1861 let client_a = server.create_client(cx_a, "user_a").await;
1862 let client_b = server.create_client(cx_b, "user_b").await;
1863 server
1864 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1865 .await;
1866
1867 client_a
1868 .fs
1869 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
1870 .await;
1871 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1872 let buffer_a = project_a
1873 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
1874 .await
1875 .unwrap();
1876
1877 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1878
1879 let buffer_b = cx_b
1880 .background()
1881 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
1882 .await
1883 .unwrap();
1884 buffer_b.update(cx_b, |buffer, cx| {
1885 buffer.edit([(4..7, "six")], None, cx);
1886 buffer.edit([(10..11, "6")], None, cx);
1887 assert_eq!(buffer.text(), "let six = 6;");
1888 assert!(buffer.is_dirty());
1889 assert!(!buffer.has_conflict());
1890 });
1891 buffer_a
1892 .condition(cx_a, |buffer, _| buffer.text() == "let six = 6;")
1893 .await;
1894
1895 client_a
1896 .fs
1897 .save(
1898 "/a/a.rs".as_ref(),
1899 &Rope::from("let seven = 7;"),
1900 LineEnding::Unix,
1901 )
1902 .await
1903 .unwrap();
1904 buffer_a
1905 .condition(cx_a, |buffer, _| buffer.has_conflict())
1906 .await;
1907 buffer_b
1908 .condition(cx_b, |buffer, _| buffer.has_conflict())
1909 .await;
1910
1911 project_b
1912 .update(cx_b, |project, cx| {
1913 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
1914 })
1915 .await
1916 .unwrap();
1917 buffer_a.read_with(cx_a, |buffer, _| {
1918 assert_eq!(buffer.text(), "let seven = 7;");
1919 assert!(!buffer.is_dirty());
1920 assert!(!buffer.has_conflict());
1921 });
1922 buffer_b.read_with(cx_b, |buffer, _| {
1923 assert_eq!(buffer.text(), "let seven = 7;");
1924 assert!(!buffer.is_dirty());
1925 assert!(!buffer.has_conflict());
1926 });
1927
1928 buffer_a.update(cx_a, |buffer, cx| {
1929 // Undoing on the host is a no-op when the reload was initiated by the guest.
1930 buffer.undo(cx);
1931 assert_eq!(buffer.text(), "let seven = 7;");
1932 assert!(!buffer.is_dirty());
1933 assert!(!buffer.has_conflict());
1934 });
1935 buffer_b.update(cx_b, |buffer, cx| {
1936 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
1937 buffer.undo(cx);
1938 assert_eq!(buffer.text(), "let six = 6;");
1939 assert!(buffer.is_dirty());
1940 assert!(!buffer.has_conflict());
1941 });
1942}
1943
1944#[gpui::test(iterations = 10)]
1945async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1946 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1947 let client_a = server.create_client(cx_a, "user_a").await;
1948 let client_b = server.create_client(cx_b, "user_b").await;
1949 server
1950 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1951 .await;
1952
1953 // Set up a fake language server.
1954 let mut language = Language::new(
1955 LanguageConfig {
1956 name: "Rust".into(),
1957 path_suffixes: vec!["rs".to_string()],
1958 ..Default::default()
1959 },
1960 Some(tree_sitter_rust::language()),
1961 );
1962 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
1963 client_a.language_registry.add(Arc::new(language));
1964
1965 // Here we insert a fake tree with a directory that exists on disk. This is needed
1966 // because later we'll invoke a command, which requires passing a working directory
1967 // that points to a valid location on disk.
1968 let directory = env::current_dir().unwrap();
1969 client_a
1970 .fs
1971 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
1972 .await;
1973 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
1974 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1975
1976 let buffer_b = cx_b
1977 .background()
1978 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
1979 .await
1980 .unwrap();
1981
1982 let fake_language_server = fake_language_servers.next().await.unwrap();
1983 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
1984 Ok(Some(vec![
1985 lsp::TextEdit {
1986 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
1987 new_text: "h".to_string(),
1988 },
1989 lsp::TextEdit {
1990 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
1991 new_text: "y".to_string(),
1992 },
1993 ]))
1994 });
1995
1996 project_b
1997 .update(cx_b, |project, cx| {
1998 project.format(HashSet::from_iter([buffer_b.clone()]), true, cx)
1999 })
2000 .await
2001 .unwrap();
2002 assert_eq!(
2003 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
2004 "let honey = \"two\""
2005 );
2006
2007 // Ensure buffer can be formatted using an external command. Notice how the
2008 // host's configuration is honored as opposed to using the guest's settings.
2009 cx_a.update(|cx| {
2010 cx.update_global(|settings: &mut Settings, _| {
2011 settings.editor_defaults.format_on_save = Some(FormatOnSave::External {
2012 command: "awk".to_string(),
2013 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
2014 });
2015 });
2016 });
2017 project_b
2018 .update(cx_b, |project, cx| {
2019 project.format(HashSet::from_iter([buffer_b.clone()]), true, cx)
2020 })
2021 .await
2022 .unwrap();
2023 assert_eq!(
2024 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
2025 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
2026 );
2027}
2028
2029#[gpui::test(iterations = 10)]
2030async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2031 cx_a.foreground().forbid_parking();
2032 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2033 let client_a = server.create_client(cx_a, "user_a").await;
2034 let client_b = server.create_client(cx_b, "user_b").await;
2035 server
2036 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2037 .await;
2038
2039 // Set up a fake language server.
2040 let mut language = Language::new(
2041 LanguageConfig {
2042 name: "Rust".into(),
2043 path_suffixes: vec!["rs".to_string()],
2044 ..Default::default()
2045 },
2046 Some(tree_sitter_rust::language()),
2047 );
2048 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2049 client_a.language_registry.add(Arc::new(language));
2050
2051 client_a
2052 .fs
2053 .insert_tree(
2054 "/root",
2055 json!({
2056 "dir-1": {
2057 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
2058 },
2059 "dir-2": {
2060 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
2061 "c.rs": "type T2 = usize;",
2062 }
2063 }),
2064 )
2065 .await;
2066 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
2067 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2068
2069 // Open the file on client B.
2070 let buffer_b = cx_b
2071 .background()
2072 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
2073 .await
2074 .unwrap();
2075
2076 // Request the definition of a symbol as the guest.
2077 let fake_language_server = fake_language_servers.next().await.unwrap();
2078 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
2079 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2080 lsp::Location::new(
2081 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
2082 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
2083 ),
2084 )))
2085 });
2086
2087 let definitions_1 = project_b
2088 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
2089 .await
2090 .unwrap();
2091 cx_b.read(|cx| {
2092 assert_eq!(definitions_1.len(), 1);
2093 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
2094 let target_buffer = definitions_1[0].target.buffer.read(cx);
2095 assert_eq!(
2096 target_buffer.text(),
2097 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
2098 );
2099 assert_eq!(
2100 definitions_1[0].target.range.to_point(target_buffer),
2101 Point::new(0, 6)..Point::new(0, 9)
2102 );
2103 });
2104
2105 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
2106 // the previous call to `definition`.
2107 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
2108 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2109 lsp::Location::new(
2110 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
2111 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
2112 ),
2113 )))
2114 });
2115
2116 let definitions_2 = project_b
2117 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
2118 .await
2119 .unwrap();
2120 cx_b.read(|cx| {
2121 assert_eq!(definitions_2.len(), 1);
2122 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
2123 let target_buffer = definitions_2[0].target.buffer.read(cx);
2124 assert_eq!(
2125 target_buffer.text(),
2126 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
2127 );
2128 assert_eq!(
2129 definitions_2[0].target.range.to_point(target_buffer),
2130 Point::new(1, 6)..Point::new(1, 11)
2131 );
2132 });
2133 assert_eq!(
2134 definitions_1[0].target.buffer,
2135 definitions_2[0].target.buffer
2136 );
2137
2138 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
2139 |req, _| async move {
2140 assert_eq!(
2141 req.text_document_position_params.position,
2142 lsp::Position::new(0, 7)
2143 );
2144 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2145 lsp::Location::new(
2146 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
2147 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
2148 ),
2149 )))
2150 },
2151 );
2152
2153 let type_definitions = project_b
2154 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
2155 .await
2156 .unwrap();
2157 cx_b.read(|cx| {
2158 assert_eq!(type_definitions.len(), 1);
2159 let target_buffer = type_definitions[0].target.buffer.read(cx);
2160 assert_eq!(target_buffer.text(), "type T2 = usize;");
2161 assert_eq!(
2162 type_definitions[0].target.range.to_point(target_buffer),
2163 Point::new(0, 5)..Point::new(0, 7)
2164 );
2165 });
2166}
2167
2168#[gpui::test(iterations = 10)]
2169async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2170 cx_a.foreground().forbid_parking();
2171 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2172 let client_a = server.create_client(cx_a, "user_a").await;
2173 let client_b = server.create_client(cx_b, "user_b").await;
2174 server
2175 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2176 .await;
2177
2178 // Set up a fake language server.
2179 let mut language = Language::new(
2180 LanguageConfig {
2181 name: "Rust".into(),
2182 path_suffixes: vec!["rs".to_string()],
2183 ..Default::default()
2184 },
2185 Some(tree_sitter_rust::language()),
2186 );
2187 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2188 client_a.language_registry.add(Arc::new(language));
2189
2190 client_a
2191 .fs
2192 .insert_tree(
2193 "/root",
2194 json!({
2195 "dir-1": {
2196 "one.rs": "const ONE: usize = 1;",
2197 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
2198 },
2199 "dir-2": {
2200 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
2201 }
2202 }),
2203 )
2204 .await;
2205 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
2206 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2207
2208 // Open the file on client B.
2209 let buffer_b = cx_b
2210 .background()
2211 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
2212 .await
2213 .unwrap();
2214
2215 // Request references to a symbol as the guest.
2216 let fake_language_server = fake_language_servers.next().await.unwrap();
2217 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
2218 assert_eq!(
2219 params.text_document_position.text_document.uri.as_str(),
2220 "file:///root/dir-1/one.rs"
2221 );
2222 Ok(Some(vec![
2223 lsp::Location {
2224 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
2225 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
2226 },
2227 lsp::Location {
2228 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
2229 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
2230 },
2231 lsp::Location {
2232 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
2233 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
2234 },
2235 ]))
2236 });
2237
2238 let references = project_b
2239 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
2240 .await
2241 .unwrap();
2242 cx_b.read(|cx| {
2243 assert_eq!(references.len(), 3);
2244 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
2245
2246 let two_buffer = references[0].buffer.read(cx);
2247 let three_buffer = references[2].buffer.read(cx);
2248 assert_eq!(
2249 two_buffer.file().unwrap().path().as_ref(),
2250 Path::new("two.rs")
2251 );
2252 assert_eq!(references[1].buffer, references[0].buffer);
2253 assert_eq!(
2254 three_buffer.file().unwrap().full_path(cx),
2255 Path::new("three.rs")
2256 );
2257
2258 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
2259 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
2260 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
2261 });
2262}
2263
2264#[gpui::test(iterations = 10)]
2265async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2266 cx_a.foreground().forbid_parking();
2267 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2268 let client_a = server.create_client(cx_a, "user_a").await;
2269 let client_b = server.create_client(cx_b, "user_b").await;
2270 server
2271 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2272 .await;
2273
2274 client_a
2275 .fs
2276 .insert_tree(
2277 "/root",
2278 json!({
2279 "dir-1": {
2280 "a": "hello world",
2281 "b": "goodnight moon",
2282 "c": "a world of goo",
2283 "d": "world champion of clown world",
2284 },
2285 "dir-2": {
2286 "e": "disney world is fun",
2287 }
2288 }),
2289 )
2290 .await;
2291 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
2292 let (worktree_2, _) = project_a
2293 .update(cx_a, |p, cx| {
2294 p.find_or_create_local_worktree("/root/dir-2", true, cx)
2295 })
2296 .await
2297 .unwrap();
2298 worktree_2
2299 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
2300 .await;
2301
2302 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2303
2304 // Perform a search as the guest.
2305 let results = project_b
2306 .update(cx_b, |project, cx| {
2307 project.search(SearchQuery::text("world", false, false), cx)
2308 })
2309 .await
2310 .unwrap();
2311
2312 let mut ranges_by_path = results
2313 .into_iter()
2314 .map(|(buffer, ranges)| {
2315 buffer.read_with(cx_b, |buffer, cx| {
2316 let path = buffer.file().unwrap().full_path(cx);
2317 let offset_ranges = ranges
2318 .into_iter()
2319 .map(|range| range.to_offset(buffer))
2320 .collect::<Vec<_>>();
2321 (path, offset_ranges)
2322 })
2323 })
2324 .collect::<Vec<_>>();
2325 ranges_by_path.sort_by_key(|(path, _)| path.clone());
2326
2327 assert_eq!(
2328 ranges_by_path,
2329 &[
2330 (PathBuf::from("dir-1/a"), vec![6..11]),
2331 (PathBuf::from("dir-1/c"), vec![2..7]),
2332 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
2333 (PathBuf::from("dir-2/e"), vec![7..12]),
2334 ]
2335 );
2336}
2337
2338#[gpui::test(iterations = 10)]
2339async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2340 cx_a.foreground().forbid_parking();
2341 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2342 let client_a = server.create_client(cx_a, "user_a").await;
2343 let client_b = server.create_client(cx_b, "user_b").await;
2344 server
2345 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2346 .await;
2347
2348 client_a
2349 .fs
2350 .insert_tree(
2351 "/root-1",
2352 json!({
2353 "main.rs": "fn double(number: i32) -> i32 { number + number }",
2354 }),
2355 )
2356 .await;
2357
2358 // Set up a fake language server.
2359 let mut language = Language::new(
2360 LanguageConfig {
2361 name: "Rust".into(),
2362 path_suffixes: vec!["rs".to_string()],
2363 ..Default::default()
2364 },
2365 Some(tree_sitter_rust::language()),
2366 );
2367 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2368 client_a.language_registry.add(Arc::new(language));
2369
2370 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
2371 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2372
2373 // Open the file on client B.
2374 let buffer_b = cx_b
2375 .background()
2376 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
2377 .await
2378 .unwrap();
2379
2380 // Request document highlights as the guest.
2381 let fake_language_server = fake_language_servers.next().await.unwrap();
2382 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
2383 |params, _| async move {
2384 assert_eq!(
2385 params
2386 .text_document_position_params
2387 .text_document
2388 .uri
2389 .as_str(),
2390 "file:///root-1/main.rs"
2391 );
2392 assert_eq!(
2393 params.text_document_position_params.position,
2394 lsp::Position::new(0, 34)
2395 );
2396 Ok(Some(vec![
2397 lsp::DocumentHighlight {
2398 kind: Some(lsp::DocumentHighlightKind::WRITE),
2399 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
2400 },
2401 lsp::DocumentHighlight {
2402 kind: Some(lsp::DocumentHighlightKind::READ),
2403 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
2404 },
2405 lsp::DocumentHighlight {
2406 kind: Some(lsp::DocumentHighlightKind::READ),
2407 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
2408 },
2409 ]))
2410 },
2411 );
2412
2413 let highlights = project_b
2414 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
2415 .await
2416 .unwrap();
2417 buffer_b.read_with(cx_b, |buffer, _| {
2418 let snapshot = buffer.snapshot();
2419
2420 let highlights = highlights
2421 .into_iter()
2422 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
2423 .collect::<Vec<_>>();
2424 assert_eq!(
2425 highlights,
2426 &[
2427 (lsp::DocumentHighlightKind::WRITE, 10..16),
2428 (lsp::DocumentHighlightKind::READ, 32..38),
2429 (lsp::DocumentHighlightKind::READ, 41..47)
2430 ]
2431 )
2432 });
2433}
2434
2435#[gpui::test(iterations = 10)]
2436async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2437 cx_a.foreground().forbid_parking();
2438 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2439 let client_a = server.create_client(cx_a, "user_a").await;
2440 let client_b = server.create_client(cx_b, "user_b").await;
2441 server
2442 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2443 .await;
2444
2445 client_a
2446 .fs
2447 .insert_tree(
2448 "/root-1",
2449 json!({
2450 "main.rs": "use std::collections::HashMap;",
2451 }),
2452 )
2453 .await;
2454
2455 // Set up a fake language server.
2456 let mut language = Language::new(
2457 LanguageConfig {
2458 name: "Rust".into(),
2459 path_suffixes: vec!["rs".to_string()],
2460 ..Default::default()
2461 },
2462 Some(tree_sitter_rust::language()),
2463 );
2464 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2465 client_a.language_registry.add(Arc::new(language));
2466
2467 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
2468 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2469
2470 // Open the file as the guest
2471 let buffer_b = cx_b
2472 .background()
2473 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
2474 .await
2475 .unwrap();
2476
2477 // Request hover information as the guest.
2478 let fake_language_server = fake_language_servers.next().await.unwrap();
2479 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
2480 |params, _| async move {
2481 assert_eq!(
2482 params
2483 .text_document_position_params
2484 .text_document
2485 .uri
2486 .as_str(),
2487 "file:///root-1/main.rs"
2488 );
2489 assert_eq!(
2490 params.text_document_position_params.position,
2491 lsp::Position::new(0, 22)
2492 );
2493 Ok(Some(lsp::Hover {
2494 contents: lsp::HoverContents::Array(vec![
2495 lsp::MarkedString::String("Test hover content.".to_string()),
2496 lsp::MarkedString::LanguageString(lsp::LanguageString {
2497 language: "Rust".to_string(),
2498 value: "let foo = 42;".to_string(),
2499 }),
2500 ]),
2501 range: Some(lsp::Range::new(
2502 lsp::Position::new(0, 22),
2503 lsp::Position::new(0, 29),
2504 )),
2505 }))
2506 },
2507 );
2508
2509 let hover_info = project_b
2510 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
2511 .await
2512 .unwrap()
2513 .unwrap();
2514 buffer_b.read_with(cx_b, |buffer, _| {
2515 let snapshot = buffer.snapshot();
2516 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
2517 assert_eq!(
2518 hover_info.contents,
2519 vec![
2520 project::HoverBlock {
2521 text: "Test hover content.".to_string(),
2522 language: None,
2523 },
2524 project::HoverBlock {
2525 text: "let foo = 42;".to_string(),
2526 language: Some("Rust".to_string()),
2527 }
2528 ]
2529 );
2530 });
2531}
2532
2533#[gpui::test(iterations = 10)]
2534async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2535 cx_a.foreground().forbid_parking();
2536 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2537 let client_a = server.create_client(cx_a, "user_a").await;
2538 let client_b = server.create_client(cx_b, "user_b").await;
2539 server
2540 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2541 .await;
2542
2543 // Set up a fake language server.
2544 let mut language = Language::new(
2545 LanguageConfig {
2546 name: "Rust".into(),
2547 path_suffixes: vec!["rs".to_string()],
2548 ..Default::default()
2549 },
2550 Some(tree_sitter_rust::language()),
2551 );
2552 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2553 client_a.language_registry.add(Arc::new(language));
2554
2555 client_a
2556 .fs
2557 .insert_tree(
2558 "/code",
2559 json!({
2560 "crate-1": {
2561 "one.rs": "const ONE: usize = 1;",
2562 },
2563 "crate-2": {
2564 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
2565 },
2566 "private": {
2567 "passwords.txt": "the-password",
2568 }
2569 }),
2570 )
2571 .await;
2572 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
2573 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2574
2575 // Cause the language server to start.
2576 let _buffer = cx_b
2577 .background()
2578 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
2579 .await
2580 .unwrap();
2581
2582 let fake_language_server = fake_language_servers.next().await.unwrap();
2583 fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(|_, _| async move {
2584 #[allow(deprecated)]
2585 Ok(Some(vec![lsp::SymbolInformation {
2586 name: "TWO".into(),
2587 location: lsp::Location {
2588 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
2589 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
2590 },
2591 kind: lsp::SymbolKind::CONSTANT,
2592 tags: None,
2593 container_name: None,
2594 deprecated: None,
2595 }]))
2596 });
2597
2598 // Request the definition of a symbol as the guest.
2599 let symbols = project_b
2600 .update(cx_b, |p, cx| p.symbols("two", cx))
2601 .await
2602 .unwrap();
2603 assert_eq!(symbols.len(), 1);
2604 assert_eq!(symbols[0].name, "TWO");
2605
2606 // Open one of the returned symbols.
2607 let buffer_b_2 = project_b
2608 .update(cx_b, |project, cx| {
2609 project.open_buffer_for_symbol(&symbols[0], cx)
2610 })
2611 .await
2612 .unwrap();
2613 buffer_b_2.read_with(cx_b, |buffer, _| {
2614 assert_eq!(
2615 buffer.file().unwrap().path().as_ref(),
2616 Path::new("../crate-2/two.rs")
2617 );
2618 });
2619
2620 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
2621 let mut fake_symbol = symbols[0].clone();
2622 fake_symbol.path.path = Path::new("/code/secrets").into();
2623 let error = project_b
2624 .update(cx_b, |project, cx| {
2625 project.open_buffer_for_symbol(&fake_symbol, cx)
2626 })
2627 .await
2628 .unwrap_err();
2629 assert!(error.to_string().contains("invalid symbol signature"));
2630}
2631
2632#[gpui::test(iterations = 10)]
2633async fn test_open_buffer_while_getting_definition_pointing_to_it(
2634 cx_a: &mut TestAppContext,
2635 cx_b: &mut TestAppContext,
2636 mut rng: StdRng,
2637) {
2638 cx_a.foreground().forbid_parking();
2639 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2640 let client_a = server.create_client(cx_a, "user_a").await;
2641 let client_b = server.create_client(cx_b, "user_b").await;
2642 server
2643 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2644 .await;
2645
2646 // Set up a fake language server.
2647 let mut language = Language::new(
2648 LanguageConfig {
2649 name: "Rust".into(),
2650 path_suffixes: vec!["rs".to_string()],
2651 ..Default::default()
2652 },
2653 Some(tree_sitter_rust::language()),
2654 );
2655 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2656 client_a.language_registry.add(Arc::new(language));
2657
2658 client_a
2659 .fs
2660 .insert_tree(
2661 "/root",
2662 json!({
2663 "a.rs": "const ONE: usize = b::TWO;",
2664 "b.rs": "const TWO: usize = 2",
2665 }),
2666 )
2667 .await;
2668 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
2669 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2670
2671 let buffer_b1 = cx_b
2672 .background()
2673 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
2674 .await
2675 .unwrap();
2676
2677 let fake_language_server = fake_language_servers.next().await.unwrap();
2678 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
2679 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2680 lsp::Location::new(
2681 lsp::Url::from_file_path("/root/b.rs").unwrap(),
2682 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
2683 ),
2684 )))
2685 });
2686
2687 let definitions;
2688 let buffer_b2;
2689 if rng.gen() {
2690 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
2691 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
2692 } else {
2693 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
2694 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
2695 }
2696
2697 let buffer_b2 = buffer_b2.await.unwrap();
2698 let definitions = definitions.await.unwrap();
2699 assert_eq!(definitions.len(), 1);
2700 assert_eq!(definitions[0].target.buffer, buffer_b2);
2701}
2702
2703#[gpui::test(iterations = 10)]
2704async fn test_collaborating_with_code_actions(
2705 cx_a: &mut TestAppContext,
2706 cx_b: &mut TestAppContext,
2707) {
2708 cx_a.foreground().forbid_parking();
2709 cx_b.update(editor::init);
2710 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2711 let client_a = server.create_client(cx_a, "user_a").await;
2712 let client_b = server.create_client(cx_b, "user_b").await;
2713 server
2714 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2715 .await;
2716
2717 // Set up a fake language server.
2718 let mut language = Language::new(
2719 LanguageConfig {
2720 name: "Rust".into(),
2721 path_suffixes: vec!["rs".to_string()],
2722 ..Default::default()
2723 },
2724 Some(tree_sitter_rust::language()),
2725 );
2726 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2727 client_a.language_registry.add(Arc::new(language));
2728
2729 client_a
2730 .fs
2731 .insert_tree(
2732 "/a",
2733 json!({
2734 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
2735 "other.rs": "pub fn foo() -> usize { 4 }",
2736 }),
2737 )
2738 .await;
2739 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2740
2741 // Join the project as client B.
2742 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2743 let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
2744 let editor_b = workspace_b
2745 .update(cx_b, |workspace, cx| {
2746 workspace.open_path((worktree_id, "main.rs"), true, cx)
2747 })
2748 .await
2749 .unwrap()
2750 .downcast::<Editor>()
2751 .unwrap();
2752
2753 let mut fake_language_server = fake_language_servers.next().await.unwrap();
2754 fake_language_server
2755 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
2756 assert_eq!(
2757 params.text_document.uri,
2758 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2759 );
2760 assert_eq!(params.range.start, lsp::Position::new(0, 0));
2761 assert_eq!(params.range.end, lsp::Position::new(0, 0));
2762 Ok(None)
2763 })
2764 .next()
2765 .await;
2766
2767 // Move cursor to a location that contains code actions.
2768 editor_b.update(cx_b, |editor, cx| {
2769 editor.change_selections(None, cx, |s| {
2770 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
2771 });
2772 cx.focus(&editor_b);
2773 });
2774
2775 fake_language_server
2776 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
2777 assert_eq!(
2778 params.text_document.uri,
2779 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2780 );
2781 assert_eq!(params.range.start, lsp::Position::new(1, 31));
2782 assert_eq!(params.range.end, lsp::Position::new(1, 31));
2783
2784 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
2785 lsp::CodeAction {
2786 title: "Inline into all callers".to_string(),
2787 edit: Some(lsp::WorkspaceEdit {
2788 changes: Some(
2789 [
2790 (
2791 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2792 vec![lsp::TextEdit::new(
2793 lsp::Range::new(
2794 lsp::Position::new(1, 22),
2795 lsp::Position::new(1, 34),
2796 ),
2797 "4".to_string(),
2798 )],
2799 ),
2800 (
2801 lsp::Url::from_file_path("/a/other.rs").unwrap(),
2802 vec![lsp::TextEdit::new(
2803 lsp::Range::new(
2804 lsp::Position::new(0, 0),
2805 lsp::Position::new(0, 27),
2806 ),
2807 "".to_string(),
2808 )],
2809 ),
2810 ]
2811 .into_iter()
2812 .collect(),
2813 ),
2814 ..Default::default()
2815 }),
2816 data: Some(json!({
2817 "codeActionParams": {
2818 "range": {
2819 "start": {"line": 1, "column": 31},
2820 "end": {"line": 1, "column": 31},
2821 }
2822 }
2823 })),
2824 ..Default::default()
2825 },
2826 )]))
2827 })
2828 .next()
2829 .await;
2830
2831 // Toggle code actions and wait for them to display.
2832 editor_b.update(cx_b, |editor, cx| {
2833 editor.toggle_code_actions(
2834 &ToggleCodeActions {
2835 deployed_from_indicator: false,
2836 },
2837 cx,
2838 );
2839 });
2840 editor_b
2841 .condition(cx_b, |editor, _| editor.context_menu_visible())
2842 .await;
2843
2844 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
2845
2846 // Confirming the code action will trigger a resolve request.
2847 let confirm_action = workspace_b
2848 .update(cx_b, |workspace, cx| {
2849 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
2850 })
2851 .unwrap();
2852 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
2853 |_, _| async move {
2854 Ok(lsp::CodeAction {
2855 title: "Inline into all callers".to_string(),
2856 edit: Some(lsp::WorkspaceEdit {
2857 changes: Some(
2858 [
2859 (
2860 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2861 vec![lsp::TextEdit::new(
2862 lsp::Range::new(
2863 lsp::Position::new(1, 22),
2864 lsp::Position::new(1, 34),
2865 ),
2866 "4".to_string(),
2867 )],
2868 ),
2869 (
2870 lsp::Url::from_file_path("/a/other.rs").unwrap(),
2871 vec![lsp::TextEdit::new(
2872 lsp::Range::new(
2873 lsp::Position::new(0, 0),
2874 lsp::Position::new(0, 27),
2875 ),
2876 "".to_string(),
2877 )],
2878 ),
2879 ]
2880 .into_iter()
2881 .collect(),
2882 ),
2883 ..Default::default()
2884 }),
2885 ..Default::default()
2886 })
2887 },
2888 );
2889
2890 // After the action is confirmed, an editor containing both modified files is opened.
2891 confirm_action.await.unwrap();
2892 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
2893 workspace
2894 .active_item(cx)
2895 .unwrap()
2896 .downcast::<Editor>()
2897 .unwrap()
2898 });
2899 code_action_editor.update(cx_b, |editor, cx| {
2900 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
2901 editor.undo(&Undo, cx);
2902 assert_eq!(
2903 editor.text(cx),
2904 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
2905 );
2906 editor.redo(&Redo, cx);
2907 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
2908 });
2909}
2910
2911#[gpui::test(iterations = 10)]
2912async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2913 cx_a.foreground().forbid_parking();
2914 cx_b.update(editor::init);
2915 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2916 let client_a = server.create_client(cx_a, "user_a").await;
2917 let client_b = server.create_client(cx_b, "user_b").await;
2918 server
2919 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2920 .await;
2921
2922 // Set up a fake language server.
2923 let mut language = Language::new(
2924 LanguageConfig {
2925 name: "Rust".into(),
2926 path_suffixes: vec!["rs".to_string()],
2927 ..Default::default()
2928 },
2929 Some(tree_sitter_rust::language()),
2930 );
2931 let mut fake_language_servers = language
2932 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2933 capabilities: lsp::ServerCapabilities {
2934 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
2935 prepare_provider: Some(true),
2936 work_done_progress_options: Default::default(),
2937 })),
2938 ..Default::default()
2939 },
2940 ..Default::default()
2941 }))
2942 .await;
2943 client_a.language_registry.add(Arc::new(language));
2944
2945 client_a
2946 .fs
2947 .insert_tree(
2948 "/dir",
2949 json!({
2950 "one.rs": "const ONE: usize = 1;",
2951 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
2952 }),
2953 )
2954 .await;
2955 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2956 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2957
2958 let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
2959 let editor_b = workspace_b
2960 .update(cx_b, |workspace, cx| {
2961 workspace.open_path((worktree_id, "one.rs"), true, cx)
2962 })
2963 .await
2964 .unwrap()
2965 .downcast::<Editor>()
2966 .unwrap();
2967 let fake_language_server = fake_language_servers.next().await.unwrap();
2968
2969 // Move cursor to a location that can be renamed.
2970 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
2971 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
2972 editor.rename(&Rename, cx).unwrap()
2973 });
2974
2975 fake_language_server
2976 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
2977 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
2978 assert_eq!(params.position, lsp::Position::new(0, 7));
2979 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
2980 lsp::Position::new(0, 6),
2981 lsp::Position::new(0, 9),
2982 ))))
2983 })
2984 .next()
2985 .await
2986 .unwrap();
2987 prepare_rename.await.unwrap();
2988 editor_b.update(cx_b, |editor, cx| {
2989 let rename = editor.pending_rename().unwrap();
2990 let buffer = editor.buffer().read(cx).snapshot(cx);
2991 assert_eq!(
2992 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
2993 6..9
2994 );
2995 rename.editor.update(cx, |rename_editor, cx| {
2996 rename_editor.buffer().update(cx, |rename_buffer, cx| {
2997 rename_buffer.edit([(0..3, "THREE")], None, cx);
2998 });
2999 });
3000 });
3001
3002 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
3003 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
3004 });
3005 fake_language_server
3006 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
3007 assert_eq!(
3008 params.text_document_position.text_document.uri.as_str(),
3009 "file:///dir/one.rs"
3010 );
3011 assert_eq!(
3012 params.text_document_position.position,
3013 lsp::Position::new(0, 6)
3014 );
3015 assert_eq!(params.new_name, "THREE");
3016 Ok(Some(lsp::WorkspaceEdit {
3017 changes: Some(
3018 [
3019 (
3020 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
3021 vec![lsp::TextEdit::new(
3022 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
3023 "THREE".to_string(),
3024 )],
3025 ),
3026 (
3027 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
3028 vec![
3029 lsp::TextEdit::new(
3030 lsp::Range::new(
3031 lsp::Position::new(0, 24),
3032 lsp::Position::new(0, 27),
3033 ),
3034 "THREE".to_string(),
3035 ),
3036 lsp::TextEdit::new(
3037 lsp::Range::new(
3038 lsp::Position::new(0, 35),
3039 lsp::Position::new(0, 38),
3040 ),
3041 "THREE".to_string(),
3042 ),
3043 ],
3044 ),
3045 ]
3046 .into_iter()
3047 .collect(),
3048 ),
3049 ..Default::default()
3050 }))
3051 })
3052 .next()
3053 .await
3054 .unwrap();
3055 confirm_rename.await.unwrap();
3056
3057 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
3058 workspace
3059 .active_item(cx)
3060 .unwrap()
3061 .downcast::<Editor>()
3062 .unwrap()
3063 });
3064 rename_editor.update(cx_b, |editor, cx| {
3065 assert_eq!(
3066 editor.text(cx),
3067 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
3068 );
3069 editor.undo(&Undo, cx);
3070 assert_eq!(
3071 editor.text(cx),
3072 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
3073 );
3074 editor.redo(&Redo, cx);
3075 assert_eq!(
3076 editor.text(cx),
3077 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
3078 );
3079 });
3080
3081 // Ensure temporary rename edits cannot be undone/redone.
3082 editor_b.update(cx_b, |editor, cx| {
3083 editor.undo(&Undo, cx);
3084 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
3085 editor.undo(&Undo, cx);
3086 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
3087 editor.redo(&Redo, cx);
3088 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
3089 })
3090}
3091
3092#[gpui::test(iterations = 10)]
3093async fn test_language_server_statuses(
3094 deterministic: Arc<Deterministic>,
3095 cx_a: &mut TestAppContext,
3096 cx_b: &mut TestAppContext,
3097) {
3098 deterministic.forbid_parking();
3099
3100 cx_b.update(editor::init);
3101 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3102 let client_a = server.create_client(cx_a, "user_a").await;
3103 let client_b = server.create_client(cx_b, "user_b").await;
3104 server
3105 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
3106 .await;
3107
3108 // Set up a fake language server.
3109 let mut language = Language::new(
3110 LanguageConfig {
3111 name: "Rust".into(),
3112 path_suffixes: vec!["rs".to_string()],
3113 ..Default::default()
3114 },
3115 Some(tree_sitter_rust::language()),
3116 );
3117 let mut fake_language_servers = language
3118 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3119 name: "the-language-server",
3120 ..Default::default()
3121 }))
3122 .await;
3123 client_a.language_registry.add(Arc::new(language));
3124
3125 client_a
3126 .fs
3127 .insert_tree(
3128 "/dir",
3129 json!({
3130 "main.rs": "const ONE: usize = 1;",
3131 }),
3132 )
3133 .await;
3134 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3135
3136 let _buffer_a = project_a
3137 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
3138 .await
3139 .unwrap();
3140
3141 let fake_language_server = fake_language_servers.next().await.unwrap();
3142 fake_language_server.start_progress("the-token").await;
3143 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3144 token: lsp::NumberOrString::String("the-token".to_string()),
3145 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
3146 lsp::WorkDoneProgressReport {
3147 message: Some("the-message".to_string()),
3148 ..Default::default()
3149 },
3150 )),
3151 });
3152 deterministic.run_until_parked();
3153 project_a.read_with(cx_a, |project, _| {
3154 let status = project.language_server_statuses().next().unwrap();
3155 assert_eq!(status.name, "the-language-server");
3156 assert_eq!(status.pending_work.len(), 1);
3157 assert_eq!(
3158 status.pending_work["the-token"].message.as_ref().unwrap(),
3159 "the-message"
3160 );
3161 });
3162
3163 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
3164 project_b.read_with(cx_b, |project, _| {
3165 let status = project.language_server_statuses().next().unwrap();
3166 assert_eq!(status.name, "the-language-server");
3167 });
3168
3169 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3170 token: lsp::NumberOrString::String("the-token".to_string()),
3171 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
3172 lsp::WorkDoneProgressReport {
3173 message: Some("the-message-2".to_string()),
3174 ..Default::default()
3175 },
3176 )),
3177 });
3178 deterministic.run_until_parked();
3179 project_a.read_with(cx_a, |project, _| {
3180 let status = project.language_server_statuses().next().unwrap();
3181 assert_eq!(status.name, "the-language-server");
3182 assert_eq!(status.pending_work.len(), 1);
3183 assert_eq!(
3184 status.pending_work["the-token"].message.as_ref().unwrap(),
3185 "the-message-2"
3186 );
3187 });
3188 project_b.read_with(cx_b, |project, _| {
3189 let status = project.language_server_statuses().next().unwrap();
3190 assert_eq!(status.name, "the-language-server");
3191 assert_eq!(status.pending_work.len(), 1);
3192 assert_eq!(
3193 status.pending_work["the-token"].message.as_ref().unwrap(),
3194 "the-message-2"
3195 );
3196 });
3197}
3198
3199#[gpui::test(iterations = 10)]
3200async fn test_basic_chat(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3201 cx_a.foreground().forbid_parking();
3202 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3203 let client_a = server.create_client(cx_a, "user_a").await;
3204 let client_b = server.create_client(cx_b, "user_b").await;
3205
3206 // Create an org that includes these 2 users.
3207 let db = &server.app_state.db;
3208 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
3209 db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
3210 .await
3211 .unwrap();
3212 db.add_org_member(org_id, client_b.current_user_id(cx_b), false)
3213 .await
3214 .unwrap();
3215
3216 // Create a channel that includes all the users.
3217 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
3218 db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
3219 .await
3220 .unwrap();
3221 db.add_channel_member(channel_id, client_b.current_user_id(cx_b), false)
3222 .await
3223 .unwrap();
3224 db.create_channel_message(
3225 channel_id,
3226 client_b.current_user_id(cx_b),
3227 "hello A, it's B.",
3228 OffsetDateTime::now_utc(),
3229 1,
3230 )
3231 .await
3232 .unwrap();
3233
3234 let channels_a =
3235 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
3236 channels_a
3237 .condition(cx_a, |list, _| list.available_channels().is_some())
3238 .await;
3239 channels_a.read_with(cx_a, |list, _| {
3240 assert_eq!(
3241 list.available_channels().unwrap(),
3242 &[ChannelDetails {
3243 id: channel_id.to_proto(),
3244 name: "test-channel".to_string()
3245 }]
3246 )
3247 });
3248 let channel_a = channels_a.update(cx_a, |this, cx| {
3249 this.get_channel(channel_id.to_proto(), cx).unwrap()
3250 });
3251 channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
3252 channel_a
3253 .condition(cx_a, |channel, _| {
3254 channel_messages(channel)
3255 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3256 })
3257 .await;
3258
3259 let channels_b =
3260 cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
3261 channels_b
3262 .condition(cx_b, |list, _| list.available_channels().is_some())
3263 .await;
3264 channels_b.read_with(cx_b, |list, _| {
3265 assert_eq!(
3266 list.available_channels().unwrap(),
3267 &[ChannelDetails {
3268 id: channel_id.to_proto(),
3269 name: "test-channel".to_string()
3270 }]
3271 )
3272 });
3273
3274 let channel_b = channels_b.update(cx_b, |this, cx| {
3275 this.get_channel(channel_id.to_proto(), cx).unwrap()
3276 });
3277 channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
3278 channel_b
3279 .condition(cx_b, |channel, _| {
3280 channel_messages(channel)
3281 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3282 })
3283 .await;
3284
3285 channel_a
3286 .update(cx_a, |channel, cx| {
3287 channel
3288 .send_message("oh, hi B.".to_string(), cx)
3289 .unwrap()
3290 .detach();
3291 let task = channel.send_message("sup".to_string(), cx).unwrap();
3292 assert_eq!(
3293 channel_messages(channel),
3294 &[
3295 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3296 ("user_a".to_string(), "oh, hi B.".to_string(), true),
3297 ("user_a".to_string(), "sup".to_string(), true)
3298 ]
3299 );
3300 task
3301 })
3302 .await
3303 .unwrap();
3304
3305 channel_b
3306 .condition(cx_b, |channel, _| {
3307 channel_messages(channel)
3308 == [
3309 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3310 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3311 ("user_a".to_string(), "sup".to_string(), false),
3312 ]
3313 })
3314 .await;
3315
3316 assert_eq!(
3317 server
3318 .store()
3319 .await
3320 .channel(channel_id)
3321 .unwrap()
3322 .connection_ids
3323 .len(),
3324 2
3325 );
3326 cx_b.update(|_| drop(channel_b));
3327 server
3328 .condition(|state| state.channel(channel_id).unwrap().connection_ids.len() == 1)
3329 .await;
3330
3331 cx_a.update(|_| drop(channel_a));
3332 server
3333 .condition(|state| state.channel(channel_id).is_none())
3334 .await;
3335}
3336
3337#[gpui::test(iterations = 10)]
3338async fn test_chat_message_validation(cx_a: &mut TestAppContext) {
3339 cx_a.foreground().forbid_parking();
3340 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3341 let client_a = server.create_client(cx_a, "user_a").await;
3342
3343 let db = &server.app_state.db;
3344 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
3345 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
3346 db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
3347 .await
3348 .unwrap();
3349 db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
3350 .await
3351 .unwrap();
3352
3353 let channels_a =
3354 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
3355 channels_a
3356 .condition(cx_a, |list, _| list.available_channels().is_some())
3357 .await;
3358 let channel_a = channels_a.update(cx_a, |this, cx| {
3359 this.get_channel(channel_id.to_proto(), cx).unwrap()
3360 });
3361
3362 // Messages aren't allowed to be too long.
3363 channel_a
3364 .update(cx_a, |channel, cx| {
3365 let long_body = "this is long.\n".repeat(1024);
3366 channel.send_message(long_body, cx).unwrap()
3367 })
3368 .await
3369 .unwrap_err();
3370
3371 // Messages aren't allowed to be blank.
3372 channel_a.update(cx_a, |channel, cx| {
3373 channel.send_message(String::new(), cx).unwrap_err()
3374 });
3375
3376 // Leading and trailing whitespace are trimmed.
3377 channel_a
3378 .update(cx_a, |channel, cx| {
3379 channel
3380 .send_message("\n surrounded by whitespace \n".to_string(), cx)
3381 .unwrap()
3382 })
3383 .await
3384 .unwrap();
3385 assert_eq!(
3386 db.get_channel_messages(channel_id, 10, None)
3387 .await
3388 .unwrap()
3389 .iter()
3390 .map(|m| &m.body)
3391 .collect::<Vec<_>>(),
3392 &["surrounded by whitespace"]
3393 );
3394}
3395
3396#[gpui::test(iterations = 10)]
3397async fn test_chat_reconnection(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3398 cx_a.foreground().forbid_parking();
3399 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3400 let client_a = server.create_client(cx_a, "user_a").await;
3401 let client_b = server.create_client(cx_b, "user_b").await;
3402
3403 let mut status_b = client_b.status();
3404
3405 // Create an org that includes these 2 users.
3406 let db = &server.app_state.db;
3407 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
3408 db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
3409 .await
3410 .unwrap();
3411 db.add_org_member(org_id, client_b.current_user_id(cx_b), false)
3412 .await
3413 .unwrap();
3414
3415 // Create a channel that includes all the users.
3416 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
3417 db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
3418 .await
3419 .unwrap();
3420 db.add_channel_member(channel_id, client_b.current_user_id(cx_b), false)
3421 .await
3422 .unwrap();
3423 db.create_channel_message(
3424 channel_id,
3425 client_b.current_user_id(cx_b),
3426 "hello A, it's B.",
3427 OffsetDateTime::now_utc(),
3428 2,
3429 )
3430 .await
3431 .unwrap();
3432
3433 let channels_a =
3434 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
3435 channels_a
3436 .condition(cx_a, |list, _| list.available_channels().is_some())
3437 .await;
3438
3439 channels_a.read_with(cx_a, |list, _| {
3440 assert_eq!(
3441 list.available_channels().unwrap(),
3442 &[ChannelDetails {
3443 id: channel_id.to_proto(),
3444 name: "test-channel".to_string()
3445 }]
3446 )
3447 });
3448 let channel_a = channels_a.update(cx_a, |this, cx| {
3449 this.get_channel(channel_id.to_proto(), cx).unwrap()
3450 });
3451 channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
3452 channel_a
3453 .condition(cx_a, |channel, _| {
3454 channel_messages(channel)
3455 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3456 })
3457 .await;
3458
3459 let channels_b =
3460 cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
3461 channels_b
3462 .condition(cx_b, |list, _| list.available_channels().is_some())
3463 .await;
3464 channels_b.read_with(cx_b, |list, _| {
3465 assert_eq!(
3466 list.available_channels().unwrap(),
3467 &[ChannelDetails {
3468 id: channel_id.to_proto(),
3469 name: "test-channel".to_string()
3470 }]
3471 )
3472 });
3473
3474 let channel_b = channels_b.update(cx_b, |this, cx| {
3475 this.get_channel(channel_id.to_proto(), cx).unwrap()
3476 });
3477 channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
3478 channel_b
3479 .condition(cx_b, |channel, _| {
3480 channel_messages(channel)
3481 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3482 })
3483 .await;
3484
3485 // Disconnect client B, ensuring we can still access its cached channel data.
3486 server.forbid_connections();
3487 server.disconnect_client(client_b.current_user_id(cx_b));
3488 cx_b.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
3489 while !matches!(
3490 status_b.next().await,
3491 Some(client::Status::ReconnectionError { .. })
3492 ) {}
3493
3494 channels_b.read_with(cx_b, |channels, _| {
3495 assert_eq!(
3496 channels.available_channels().unwrap(),
3497 [ChannelDetails {
3498 id: channel_id.to_proto(),
3499 name: "test-channel".to_string()
3500 }]
3501 )
3502 });
3503 channel_b.read_with(cx_b, |channel, _| {
3504 assert_eq!(
3505 channel_messages(channel),
3506 [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3507 )
3508 });
3509
3510 // Send a message from client B while it is disconnected.
3511 channel_b
3512 .update(cx_b, |channel, cx| {
3513 let task = channel
3514 .send_message("can you see this?".to_string(), cx)
3515 .unwrap();
3516 assert_eq!(
3517 channel_messages(channel),
3518 &[
3519 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3520 ("user_b".to_string(), "can you see this?".to_string(), true)
3521 ]
3522 );
3523 task
3524 })
3525 .await
3526 .unwrap_err();
3527
3528 // Send a message from client A while B is disconnected.
3529 channel_a
3530 .update(cx_a, |channel, cx| {
3531 channel
3532 .send_message("oh, hi B.".to_string(), cx)
3533 .unwrap()
3534 .detach();
3535 let task = channel.send_message("sup".to_string(), cx).unwrap();
3536 assert_eq!(
3537 channel_messages(channel),
3538 &[
3539 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3540 ("user_a".to_string(), "oh, hi B.".to_string(), true),
3541 ("user_a".to_string(), "sup".to_string(), true)
3542 ]
3543 );
3544 task
3545 })
3546 .await
3547 .unwrap();
3548
3549 // Give client B a chance to reconnect.
3550 server.allow_connections();
3551 cx_b.foreground().advance_clock(Duration::from_secs(10));
3552
3553 // Verify that B sees the new messages upon reconnection, as well as the message client B
3554 // sent while offline.
3555 channel_b
3556 .condition(cx_b, |channel, _| {
3557 channel_messages(channel)
3558 == [
3559 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3560 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3561 ("user_a".to_string(), "sup".to_string(), false),
3562 ("user_b".to_string(), "can you see this?".to_string(), false),
3563 ]
3564 })
3565 .await;
3566
3567 // Ensure client A and B can communicate normally after reconnection.
3568 channel_a
3569 .update(cx_a, |channel, cx| {
3570 channel.send_message("you online?".to_string(), cx).unwrap()
3571 })
3572 .await
3573 .unwrap();
3574 channel_b
3575 .condition(cx_b, |channel, _| {
3576 channel_messages(channel)
3577 == [
3578 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3579 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3580 ("user_a".to_string(), "sup".to_string(), false),
3581 ("user_b".to_string(), "can you see this?".to_string(), false),
3582 ("user_a".to_string(), "you online?".to_string(), false),
3583 ]
3584 })
3585 .await;
3586
3587 channel_b
3588 .update(cx_b, |channel, cx| {
3589 channel.send_message("yep".to_string(), cx).unwrap()
3590 })
3591 .await
3592 .unwrap();
3593 channel_a
3594 .condition(cx_a, |channel, _| {
3595 channel_messages(channel)
3596 == [
3597 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3598 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3599 ("user_a".to_string(), "sup".to_string(), false),
3600 ("user_b".to_string(), "can you see this?".to_string(), false),
3601 ("user_a".to_string(), "you online?".to_string(), false),
3602 ("user_b".to_string(), "yep".to_string(), false),
3603 ]
3604 })
3605 .await;
3606}
3607
3608#[gpui::test(iterations = 10)]
3609async fn test_contacts(
3610 deterministic: Arc<Deterministic>,
3611 cx_a: &mut TestAppContext,
3612 cx_b: &mut TestAppContext,
3613 cx_c: &mut TestAppContext,
3614) {
3615 cx_a.foreground().forbid_parking();
3616 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3617 let client_a = server.create_client(cx_a, "user_a").await;
3618 let client_b = server.create_client(cx_b, "user_b").await;
3619 let client_c = server.create_client(cx_c, "user_c").await;
3620 server
3621 .make_contacts(vec![
3622 (&client_a, cx_a),
3623 (&client_b, cx_b),
3624 (&client_c, cx_c),
3625 ])
3626 .await;
3627
3628 deterministic.run_until_parked();
3629 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3630 client.user_store.read_with(*cx, |store, _| {
3631 assert_eq!(
3632 contacts(store),
3633 [
3634 ("user_a", true, vec![]),
3635 ("user_b", true, vec![]),
3636 ("user_c", true, vec![])
3637 ],
3638 "{} has the wrong contacts",
3639 client.username
3640 )
3641 });
3642 }
3643
3644 // Share a project as client A.
3645 client_a.fs.create_dir(Path::new("/a")).await.unwrap();
3646 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3647
3648 deterministic.run_until_parked();
3649 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3650 client.user_store.read_with(*cx, |store, _| {
3651 assert_eq!(
3652 contacts(store),
3653 [
3654 ("user_a", true, vec![("a", vec![])]),
3655 ("user_b", true, vec![]),
3656 ("user_c", true, vec![])
3657 ],
3658 "{} has the wrong contacts",
3659 client.username
3660 )
3661 });
3662 }
3663
3664 let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
3665
3666 deterministic.run_until_parked();
3667 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3668 client.user_store.read_with(*cx, |store, _| {
3669 assert_eq!(
3670 contacts(store),
3671 [
3672 ("user_a", true, vec![("a", vec!["user_b"])]),
3673 ("user_b", true, vec![]),
3674 ("user_c", true, vec![])
3675 ],
3676 "{} has the wrong contacts",
3677 client.username
3678 )
3679 });
3680 }
3681
3682 // Add a local project as client B
3683 client_a.fs.create_dir("/b".as_ref()).await.unwrap();
3684 let (_project_b, _) = client_b.build_local_project("/b", cx_b).await;
3685
3686 deterministic.run_until_parked();
3687 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3688 client.user_store.read_with(*cx, |store, _| {
3689 assert_eq!(
3690 contacts(store),
3691 [
3692 ("user_a", true, vec![("a", vec!["user_b"])]),
3693 ("user_b", true, vec![("b", vec![])]),
3694 ("user_c", true, vec![])
3695 ],
3696 "{} has the wrong contacts",
3697 client.username
3698 )
3699 });
3700 }
3701
3702 project_a
3703 .condition(cx_a, |project, _| {
3704 project.collaborators().contains_key(&client_b.peer_id)
3705 })
3706 .await;
3707
3708 cx_a.update(move |_| drop(project_a));
3709 deterministic.run_until_parked();
3710 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3711 client.user_store.read_with(*cx, |store, _| {
3712 assert_eq!(
3713 contacts(store),
3714 [
3715 ("user_a", true, vec![]),
3716 ("user_b", true, vec![("b", vec![])]),
3717 ("user_c", true, vec![])
3718 ],
3719 "{} has the wrong contacts",
3720 client.username
3721 )
3722 });
3723 }
3724
3725 server.disconnect_client(client_c.current_user_id(cx_c));
3726 server.forbid_connections();
3727 deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
3728 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b)] {
3729 client.user_store.read_with(*cx, |store, _| {
3730 assert_eq!(
3731 contacts(store),
3732 [
3733 ("user_a", true, vec![]),
3734 ("user_b", true, vec![("b", vec![])]),
3735 ("user_c", false, vec![])
3736 ],
3737 "{} has the wrong contacts",
3738 client.username
3739 )
3740 });
3741 }
3742 client_c
3743 .user_store
3744 .read_with(cx_c, |store, _| assert_eq!(contacts(store), []));
3745
3746 server.allow_connections();
3747 client_c
3748 .authenticate_and_connect(false, &cx_c.to_async())
3749 .await
3750 .unwrap();
3751
3752 deterministic.run_until_parked();
3753 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3754 client.user_store.read_with(*cx, |store, _| {
3755 assert_eq!(
3756 contacts(store),
3757 [
3758 ("user_a", true, vec![]),
3759 ("user_b", true, vec![("b", vec![])]),
3760 ("user_c", true, vec![])
3761 ],
3762 "{} has the wrong contacts",
3763 client.username
3764 )
3765 });
3766 }
3767
3768 #[allow(clippy::type_complexity)]
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 let pane_a1 = pane_a1.clone();
4250 cx.defer(move |workspace, _| {
4251 assert_ne!(*workspace.active_pane(), pane_a1);
4252 });
4253 });
4254 workspace_a
4255 .update(cx_a, |workspace, cx| {
4256 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
4257 workspace
4258 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
4259 .unwrap()
4260 })
4261 .await
4262 .unwrap();
4263 workspace_b.update(cx_b, |workspace, cx| {
4264 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
4265 let pane_b1 = pane_b1.clone();
4266 cx.defer(move |workspace, _| {
4267 assert_ne!(*workspace.active_pane(), pane_b1);
4268 });
4269 });
4270 workspace_b
4271 .update(cx_b, |workspace, cx| {
4272 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
4273 workspace
4274 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
4275 .unwrap()
4276 })
4277 .await
4278 .unwrap();
4279
4280 workspace_a.update(cx_a, |workspace, cx| {
4281 workspace.activate_next_pane(cx);
4282 });
4283 // Wait for focus effects to be fully flushed
4284 workspace_a.update(cx_a, |workspace, _| {
4285 assert_eq!(*workspace.active_pane(), pane_a1);
4286 });
4287
4288 workspace_a
4289 .update(cx_a, |workspace, cx| {
4290 workspace.open_path((worktree_id, "3.txt"), true, cx)
4291 })
4292 .await
4293 .unwrap();
4294 workspace_b.update(cx_b, |workspace, cx| {
4295 workspace.activate_next_pane(cx);
4296 });
4297
4298 workspace_b
4299 .update(cx_b, |workspace, cx| {
4300 assert_eq!(*workspace.active_pane(), pane_b1);
4301 workspace.open_path((worktree_id, "4.txt"), true, cx)
4302 })
4303 .await
4304 .unwrap();
4305 cx_a.foreground().run_until_parked();
4306
4307 // Ensure leader updates don't change the active pane of followers
4308 workspace_a.read_with(cx_a, |workspace, _| {
4309 assert_eq!(*workspace.active_pane(), pane_a1);
4310 });
4311 workspace_b.read_with(cx_b, |workspace, _| {
4312 assert_eq!(*workspace.active_pane(), pane_b1);
4313 });
4314
4315 // Ensure peers following each other doesn't cause an infinite loop.
4316 assert_eq!(
4317 workspace_a.read_with(cx_a, |workspace, cx| workspace
4318 .active_item(cx)
4319 .unwrap()
4320 .project_path(cx)),
4321 Some((worktree_id, "3.txt").into())
4322 );
4323 workspace_a.update(cx_a, |workspace, cx| {
4324 assert_eq!(
4325 workspace.active_item(cx).unwrap().project_path(cx),
4326 Some((worktree_id, "3.txt").into())
4327 );
4328 workspace.activate_next_pane(cx);
4329 });
4330
4331 workspace_a.update(cx_a, |workspace, cx| {
4332 assert_eq!(
4333 workspace.active_item(cx).unwrap().project_path(cx),
4334 Some((worktree_id, "4.txt").into())
4335 );
4336 });
4337
4338 workspace_b.update(cx_b, |workspace, cx| {
4339 assert_eq!(
4340 workspace.active_item(cx).unwrap().project_path(cx),
4341 Some((worktree_id, "4.txt").into())
4342 );
4343 workspace.activate_next_pane(cx);
4344 });
4345
4346 workspace_b.update(cx_b, |workspace, cx| {
4347 assert_eq!(
4348 workspace.active_item(cx).unwrap().project_path(cx),
4349 Some((worktree_id, "3.txt").into())
4350 );
4351 });
4352}
4353
4354#[gpui::test(iterations = 10)]
4355async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4356 cx_a.foreground().forbid_parking();
4357
4358 // 2 clients connect to a server.
4359 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4360 let client_a = server.create_client(cx_a, "user_a").await;
4361 let client_b = server.create_client(cx_b, "user_b").await;
4362 server
4363 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
4364 .await;
4365 cx_a.update(editor::init);
4366 cx_b.update(editor::init);
4367
4368 // Client A shares a project.
4369 client_a
4370 .fs
4371 .insert_tree(
4372 "/a",
4373 json!({
4374 "1.txt": "one",
4375 "2.txt": "two",
4376 "3.txt": "three",
4377 }),
4378 )
4379 .await;
4380 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4381 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
4382
4383 // Client A opens some editors.
4384 let workspace_a = client_a.build_workspace(&project_a, cx_a);
4385 let _editor_a1 = workspace_a
4386 .update(cx_a, |workspace, cx| {
4387 workspace.open_path((worktree_id, "1.txt"), true, cx)
4388 })
4389 .await
4390 .unwrap()
4391 .downcast::<Editor>()
4392 .unwrap();
4393
4394 // Client B starts following client A.
4395 let workspace_b = client_b.build_workspace(&project_b, cx_b);
4396 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
4397 let leader_id = project_b.read_with(cx_b, |project, _| {
4398 project.collaborators().values().next().unwrap().peer_id
4399 });
4400 workspace_b
4401 .update(cx_b, |workspace, cx| {
4402 workspace
4403 .toggle_follow(&ToggleFollow(leader_id), cx)
4404 .unwrap()
4405 })
4406 .await
4407 .unwrap();
4408 assert_eq!(
4409 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4410 Some(leader_id)
4411 );
4412 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
4413 workspace
4414 .active_item(cx)
4415 .unwrap()
4416 .downcast::<Editor>()
4417 .unwrap()
4418 });
4419
4420 // When client B moves, it automatically stops following client A.
4421 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
4422 assert_eq!(
4423 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4424 None
4425 );
4426
4427 workspace_b
4428 .update(cx_b, |workspace, cx| {
4429 workspace
4430 .toggle_follow(&ToggleFollow(leader_id), cx)
4431 .unwrap()
4432 })
4433 .await
4434 .unwrap();
4435 assert_eq!(
4436 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4437 Some(leader_id)
4438 );
4439
4440 // When client B edits, it automatically stops following client A.
4441 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
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 scrolls, it automatically stops following client A.
4461 editor_b2.update(cx_b, |editor, cx| {
4462 editor.set_scroll_position(vec2f(0., 3.), cx)
4463 });
4464 assert_eq!(
4465 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4466 None
4467 );
4468
4469 workspace_b
4470 .update(cx_b, |workspace, cx| {
4471 workspace
4472 .toggle_follow(&ToggleFollow(leader_id), cx)
4473 .unwrap()
4474 })
4475 .await
4476 .unwrap();
4477 assert_eq!(
4478 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4479 Some(leader_id)
4480 );
4481
4482 // When client B activates a different pane, it continues following client A in the original pane.
4483 workspace_b.update(cx_b, |workspace, cx| {
4484 workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
4485 });
4486 assert_eq!(
4487 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4488 Some(leader_id)
4489 );
4490
4491 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
4492 assert_eq!(
4493 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4494 Some(leader_id)
4495 );
4496
4497 // When client B activates a different item in the original pane, it automatically stops following client A.
4498 workspace_b
4499 .update(cx_b, |workspace, cx| {
4500 workspace.open_path((worktree_id, "2.txt"), true, cx)
4501 })
4502 .await
4503 .unwrap();
4504 assert_eq!(
4505 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4506 None
4507 );
4508}
4509
4510#[gpui::test(iterations = 10)]
4511async fn test_peers_simultaneously_following_each_other(
4512 deterministic: Arc<Deterministic>,
4513 cx_a: &mut TestAppContext,
4514 cx_b: &mut TestAppContext,
4515) {
4516 deterministic.forbid_parking();
4517
4518 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4519 let client_a = server.create_client(cx_a, "user_a").await;
4520 let client_b = server.create_client(cx_b, "user_b").await;
4521 server
4522 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
4523 .await;
4524 cx_a.update(editor::init);
4525 cx_b.update(editor::init);
4526
4527 client_a.fs.insert_tree("/a", json!({})).await;
4528 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
4529 let workspace_a = client_a.build_workspace(&project_a, cx_a);
4530
4531 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
4532 let workspace_b = client_b.build_workspace(&project_b, cx_b);
4533
4534 deterministic.run_until_parked();
4535 let client_a_id = project_b.read_with(cx_b, |project, _| {
4536 project.collaborators().values().next().unwrap().peer_id
4537 });
4538 let client_b_id = project_a.read_with(cx_a, |project, _| {
4539 project.collaborators().values().next().unwrap().peer_id
4540 });
4541
4542 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
4543 workspace
4544 .toggle_follow(&ToggleFollow(client_b_id), cx)
4545 .unwrap()
4546 });
4547 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
4548 workspace
4549 .toggle_follow(&ToggleFollow(client_a_id), cx)
4550 .unwrap()
4551 });
4552
4553 futures::try_join!(a_follow_b, b_follow_a).unwrap();
4554 workspace_a.read_with(cx_a, |workspace, _| {
4555 assert_eq!(
4556 workspace.leader_for_pane(workspace.active_pane()),
4557 Some(client_b_id)
4558 );
4559 });
4560 workspace_b.read_with(cx_b, |workspace, _| {
4561 assert_eq!(
4562 workspace.leader_for_pane(workspace.active_pane()),
4563 Some(client_a_id)
4564 );
4565 });
4566}
4567
4568#[gpui::test(iterations = 100)]
4569async fn test_random_collaboration(
4570 cx: &mut TestAppContext,
4571 deterministic: Arc<Deterministic>,
4572 rng: StdRng,
4573) {
4574 deterministic.forbid_parking();
4575 let max_peers = env::var("MAX_PEERS")
4576 .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
4577 .unwrap_or(5);
4578 assert!(max_peers <= 5);
4579
4580 let max_operations = env::var("OPERATIONS")
4581 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
4582 .unwrap_or(10);
4583
4584 let rng = Arc::new(Mutex::new(rng));
4585
4586 let guest_lang_registry = Arc::new(LanguageRegistry::test());
4587 let host_language_registry = Arc::new(LanguageRegistry::test());
4588
4589 let fs = FakeFs::new(cx.background());
4590 fs.insert_tree("/_collab", json!({"init": ""})).await;
4591
4592 let mut server = TestServer::start(cx.foreground(), cx.background()).await;
4593 let db = server.app_state.db.clone();
4594 let host_user_id = db.create_user("host", None, false).await.unwrap();
4595 let mut available_guests = vec![
4596 "guest-1".to_string(),
4597 "guest-2".to_string(),
4598 "guest-3".to_string(),
4599 "guest-4".to_string(),
4600 ];
4601
4602 for username in &available_guests {
4603 let guest_user_id = db.create_user(username, None, false).await.unwrap();
4604 assert_eq!(*username, format!("guest-{}", guest_user_id));
4605 server
4606 .app_state
4607 .db
4608 .send_contact_request(guest_user_id, host_user_id)
4609 .await
4610 .unwrap();
4611 server
4612 .app_state
4613 .db
4614 .respond_to_contact_request(host_user_id, guest_user_id, true)
4615 .await
4616 .unwrap();
4617 }
4618
4619 let mut clients = Vec::new();
4620 let mut user_ids = Vec::new();
4621 let mut op_start_signals = Vec::new();
4622
4623 let mut next_entity_id = 100000;
4624 let mut host_cx = TestAppContext::new(
4625 cx.foreground_platform(),
4626 cx.platform(),
4627 deterministic.build_foreground(next_entity_id),
4628 deterministic.build_background(),
4629 cx.font_cache(),
4630 cx.leak_detector(),
4631 next_entity_id,
4632 );
4633 let host = server.create_client(&mut host_cx, "host").await;
4634 let host_project = host_cx.update(|cx| {
4635 Project::local(
4636 true,
4637 host.client.clone(),
4638 host.user_store.clone(),
4639 host.project_store.clone(),
4640 host_language_registry.clone(),
4641 fs.clone(),
4642 cx,
4643 )
4644 });
4645 let host_project_id = host_project
4646 .update(&mut host_cx, |p, _| p.next_remote_id())
4647 .await;
4648
4649 let (collab_worktree, _) = host_project
4650 .update(&mut host_cx, |project, cx| {
4651 project.find_or_create_local_worktree("/_collab", true, cx)
4652 })
4653 .await
4654 .unwrap();
4655 collab_worktree
4656 .read_with(&host_cx, |tree, _| tree.as_local().unwrap().scan_complete())
4657 .await;
4658
4659 // Set up fake language servers.
4660 let mut language = Language::new(
4661 LanguageConfig {
4662 name: "Rust".into(),
4663 path_suffixes: vec!["rs".to_string()],
4664 ..Default::default()
4665 },
4666 None,
4667 );
4668 let _fake_servers = language
4669 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4670 name: "the-fake-language-server",
4671 capabilities: lsp::LanguageServer::full_capabilities(),
4672 initializer: Some(Box::new({
4673 let rng = rng.clone();
4674 let fs = fs.clone();
4675 let project = host_project.downgrade();
4676 move |fake_server: &mut FakeLanguageServer| {
4677 fake_server.handle_request::<lsp::request::Completion, _, _>(
4678 |_, _| async move {
4679 Ok(Some(lsp::CompletionResponse::Array(vec![
4680 lsp::CompletionItem {
4681 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4682 range: lsp::Range::new(
4683 lsp::Position::new(0, 0),
4684 lsp::Position::new(0, 0),
4685 ),
4686 new_text: "the-new-text".to_string(),
4687 })),
4688 ..Default::default()
4689 },
4690 ])))
4691 },
4692 );
4693
4694 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
4695 |_, _| async move {
4696 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
4697 lsp::CodeAction {
4698 title: "the-code-action".to_string(),
4699 ..Default::default()
4700 },
4701 )]))
4702 },
4703 );
4704
4705 fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
4706 |params, _| async move {
4707 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
4708 params.position,
4709 params.position,
4710 ))))
4711 },
4712 );
4713
4714 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
4715 let fs = fs.clone();
4716 let rng = rng.clone();
4717 move |_, _| {
4718 let fs = fs.clone();
4719 let rng = rng.clone();
4720 async move {
4721 let files = fs.files().await;
4722 let mut rng = rng.lock();
4723 let count = rng.gen_range::<usize, _>(1..3);
4724 let files = (0..count)
4725 .map(|_| files.choose(&mut *rng).unwrap())
4726 .collect::<Vec<_>>();
4727 log::info!("LSP: Returning definitions in files {:?}", &files);
4728 Ok(Some(lsp::GotoDefinitionResponse::Array(
4729 files
4730 .into_iter()
4731 .map(|file| lsp::Location {
4732 uri: lsp::Url::from_file_path(file).unwrap(),
4733 range: Default::default(),
4734 })
4735 .collect(),
4736 )))
4737 }
4738 }
4739 });
4740
4741 fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
4742 let rng = rng.clone();
4743 let project = project;
4744 move |params, mut cx| {
4745 let highlights = if let Some(project) = project.upgrade(&cx) {
4746 project.update(&mut cx, |project, cx| {
4747 let path = params
4748 .text_document_position_params
4749 .text_document
4750 .uri
4751 .to_file_path()
4752 .unwrap();
4753 let (worktree, relative_path) =
4754 project.find_local_worktree(&path, cx)?;
4755 let project_path =
4756 ProjectPath::from((worktree.read(cx).id(), relative_path));
4757 let buffer =
4758 project.get_open_buffer(&project_path, cx)?.read(cx);
4759
4760 let mut highlights = Vec::new();
4761 let highlight_count = rng.lock().gen_range(1..=5);
4762 let mut prev_end = 0;
4763 for _ in 0..highlight_count {
4764 let range =
4765 buffer.random_byte_range(prev_end, &mut *rng.lock());
4766
4767 highlights.push(lsp::DocumentHighlight {
4768 range: range_to_lsp(range.to_point_utf16(buffer)),
4769 kind: Some(lsp::DocumentHighlightKind::READ),
4770 });
4771 prev_end = range.end;
4772 }
4773 Some(highlights)
4774 })
4775 } else {
4776 None
4777 };
4778 async move { Ok(highlights) }
4779 }
4780 });
4781 }
4782 })),
4783 ..Default::default()
4784 }))
4785 .await;
4786 host_language_registry.add(Arc::new(language));
4787
4788 let op_start_signal = futures::channel::mpsc::unbounded();
4789 user_ids.push(host.current_user_id(&host_cx));
4790 op_start_signals.push(op_start_signal.0);
4791 clients.push(host_cx.foreground().spawn(host.simulate_host(
4792 host_project,
4793 op_start_signal.1,
4794 rng.clone(),
4795 host_cx,
4796 )));
4797
4798 let disconnect_host_at = if rng.lock().gen_bool(0.2) {
4799 rng.lock().gen_range(0..max_operations)
4800 } else {
4801 max_operations
4802 };
4803
4804 let mut operations = 0;
4805 while operations < max_operations {
4806 if operations == disconnect_host_at {
4807 server.disconnect_client(user_ids[0]);
4808 deterministic.advance_clock(RECEIVE_TIMEOUT);
4809 drop(op_start_signals);
4810
4811 deterministic.start_waiting();
4812 let mut clients = futures::future::join_all(clients).await;
4813 deterministic.finish_waiting();
4814 deterministic.run_until_parked();
4815
4816 let (host, host_project, mut host_cx, host_err) = clients.remove(0);
4817 if let Some(host_err) = host_err {
4818 log::error!("host error - {:?}", host_err);
4819 }
4820 host_project.read_with(&host_cx, |project, _| assert!(!project.is_shared()));
4821 for (guest, guest_project, mut guest_cx, guest_err) in clients {
4822 if let Some(guest_err) = guest_err {
4823 log::error!("{} error - {:?}", guest.username, guest_err);
4824 }
4825
4826 let contacts = server
4827 .app_state
4828 .db
4829 .get_contacts(guest.current_user_id(&guest_cx))
4830 .await
4831 .unwrap();
4832 let contacts = server
4833 .store
4834 .lock()
4835 .await
4836 .build_initial_contacts_update(contacts)
4837 .contacts;
4838 assert!(!contacts
4839 .iter()
4840 .flat_map(|contact| &contact.projects)
4841 .any(|project| project.id == host_project_id));
4842 guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
4843 guest_cx.update(|_| drop((guest, guest_project)));
4844 }
4845 host_cx.update(|_| drop((host, host_project)));
4846
4847 return;
4848 }
4849
4850 let distribution = rng.lock().gen_range(0..100);
4851 match distribution {
4852 0..=19 if !available_guests.is_empty() => {
4853 let guest_ix = rng.lock().gen_range(0..available_guests.len());
4854 let guest_username = available_guests.remove(guest_ix);
4855 log::info!("Adding new connection for {}", guest_username);
4856 next_entity_id += 100000;
4857 let mut guest_cx = TestAppContext::new(
4858 cx.foreground_platform(),
4859 cx.platform(),
4860 deterministic.build_foreground(next_entity_id),
4861 deterministic.build_background(),
4862 cx.font_cache(),
4863 cx.leak_detector(),
4864 next_entity_id,
4865 );
4866
4867 deterministic.start_waiting();
4868 let guest = server.create_client(&mut guest_cx, &guest_username).await;
4869 let guest_project = Project::remote(
4870 host_project_id,
4871 guest.client.clone(),
4872 guest.user_store.clone(),
4873 guest.project_store.clone(),
4874 guest_lang_registry.clone(),
4875 FakeFs::new(cx.background()),
4876 guest_cx.to_async(),
4877 )
4878 .await
4879 .unwrap();
4880 deterministic.finish_waiting();
4881
4882 let op_start_signal = futures::channel::mpsc::unbounded();
4883 user_ids.push(guest.current_user_id(&guest_cx));
4884 op_start_signals.push(op_start_signal.0);
4885 clients.push(guest_cx.foreground().spawn(guest.simulate_guest(
4886 guest_username.clone(),
4887 guest_project,
4888 op_start_signal.1,
4889 rng.clone(),
4890 guest_cx,
4891 )));
4892
4893 log::info!("Added connection for {}", guest_username);
4894 operations += 1;
4895 }
4896 20..=29 if clients.len() > 1 => {
4897 let guest_ix = rng.lock().gen_range(1..clients.len());
4898 log::info!("Removing guest {}", user_ids[guest_ix]);
4899 let removed_guest_id = user_ids.remove(guest_ix);
4900 let guest = clients.remove(guest_ix);
4901 op_start_signals.remove(guest_ix);
4902 server.forbid_connections();
4903 server.disconnect_client(removed_guest_id);
4904 deterministic.advance_clock(RECEIVE_TIMEOUT);
4905 deterministic.start_waiting();
4906 log::info!("Waiting for guest {} to exit...", removed_guest_id);
4907 let (guest, guest_project, mut guest_cx, guest_err) = guest.await;
4908 deterministic.finish_waiting();
4909 server.allow_connections();
4910
4911 if let Some(guest_err) = guest_err {
4912 log::error!("{} error - {:?}", guest.username, guest_err);
4913 }
4914 guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
4915 for user_id in &user_ids {
4916 let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
4917 let contacts = server
4918 .store
4919 .lock()
4920 .await
4921 .build_initial_contacts_update(contacts)
4922 .contacts;
4923 for contact in contacts {
4924 if contact.online {
4925 assert_ne!(
4926 contact.user_id, removed_guest_id.0 as u64,
4927 "removed guest is still a contact of another peer"
4928 );
4929 }
4930 for project in contact.projects {
4931 for project_guest_id in project.guests {
4932 assert_ne!(
4933 project_guest_id, removed_guest_id.0 as u64,
4934 "removed guest appears as still participating on a project"
4935 );
4936 }
4937 }
4938 }
4939 }
4940
4941 log::info!("{} removed", guest.username);
4942 available_guests.push(guest.username.clone());
4943 guest_cx.update(|_| drop((guest, guest_project)));
4944
4945 operations += 1;
4946 }
4947 _ => {
4948 while operations < max_operations && rng.lock().gen_bool(0.7) {
4949 op_start_signals
4950 .choose(&mut *rng.lock())
4951 .unwrap()
4952 .unbounded_send(())
4953 .unwrap();
4954 operations += 1;
4955 }
4956
4957 if rng.lock().gen_bool(0.8) {
4958 deterministic.run_until_parked();
4959 }
4960 }
4961 }
4962 }
4963
4964 drop(op_start_signals);
4965 deterministic.start_waiting();
4966 let mut clients = futures::future::join_all(clients).await;
4967 deterministic.finish_waiting();
4968 deterministic.run_until_parked();
4969
4970 let (host_client, host_project, mut host_cx, host_err) = clients.remove(0);
4971 if let Some(host_err) = host_err {
4972 panic!("host error - {:?}", host_err);
4973 }
4974 let host_worktree_snapshots = host_project.read_with(&host_cx, |project, cx| {
4975 project
4976 .worktrees(cx)
4977 .map(|worktree| {
4978 let snapshot = worktree.read(cx).snapshot();
4979 (snapshot.id(), snapshot)
4980 })
4981 .collect::<BTreeMap<_, _>>()
4982 });
4983
4984 host_project.read_with(&host_cx, |project, cx| project.check_invariants(cx));
4985
4986 for (guest_client, guest_project, mut guest_cx, guest_err) in clients.into_iter() {
4987 if let Some(guest_err) = guest_err {
4988 panic!("{} error - {:?}", guest_client.username, guest_err);
4989 }
4990 let worktree_snapshots = guest_project.read_with(&guest_cx, |project, cx| {
4991 project
4992 .worktrees(cx)
4993 .map(|worktree| {
4994 let worktree = worktree.read(cx);
4995 (worktree.id(), worktree.snapshot())
4996 })
4997 .collect::<BTreeMap<_, _>>()
4998 });
4999
5000 assert_eq!(
5001 worktree_snapshots.keys().collect::<Vec<_>>(),
5002 host_worktree_snapshots.keys().collect::<Vec<_>>(),
5003 "{} has different worktrees than the host",
5004 guest_client.username
5005 );
5006 for (id, host_snapshot) in &host_worktree_snapshots {
5007 let guest_snapshot = &worktree_snapshots[id];
5008 assert_eq!(
5009 guest_snapshot.root_name(),
5010 host_snapshot.root_name(),
5011 "{} has different root name than the host for worktree {}",
5012 guest_client.username,
5013 id
5014 );
5015 assert_eq!(
5016 guest_snapshot.entries(false).collect::<Vec<_>>(),
5017 host_snapshot.entries(false).collect::<Vec<_>>(),
5018 "{} has different snapshot than the host for worktree {}",
5019 guest_client.username,
5020 id
5021 );
5022 assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id());
5023 }
5024
5025 guest_project.read_with(&guest_cx, |project, cx| project.check_invariants(cx));
5026
5027 for guest_buffer in &guest_client.buffers {
5028 let buffer_id = guest_buffer.read_with(&guest_cx, |buffer, _| buffer.remote_id());
5029 let host_buffer = host_project.read_with(&host_cx, |project, cx| {
5030 project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
5031 panic!(
5032 "host does not have buffer for guest:{}, peer:{}, id:{}",
5033 guest_client.username, guest_client.peer_id, buffer_id
5034 )
5035 })
5036 });
5037 let path =
5038 host_buffer.read_with(&host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
5039
5040 assert_eq!(
5041 guest_buffer.read_with(&guest_cx, |buffer, _| buffer.deferred_ops_len()),
5042 0,
5043 "{}, buffer {}, path {:?} has deferred operations",
5044 guest_client.username,
5045 buffer_id,
5046 path,
5047 );
5048 assert_eq!(
5049 guest_buffer.read_with(&guest_cx, |buffer, _| buffer.text()),
5050 host_buffer.read_with(&host_cx, |buffer, _| buffer.text()),
5051 "{}, buffer {}, path {:?}, differs from the host's buffer",
5052 guest_client.username,
5053 buffer_id,
5054 path
5055 );
5056 }
5057
5058 guest_cx.update(|_| drop((guest_project, guest_client)));
5059 }
5060
5061 host_cx.update(|_| drop((host_client, host_project)));
5062}
5063
5064struct TestServer {
5065 peer: Arc<Peer>,
5066 app_state: Arc<AppState>,
5067 server: Arc<Server>,
5068 foreground: Rc<executor::Foreground>,
5069 notifications: mpsc::UnboundedReceiver<()>,
5070 connection_killers: Arc<Mutex<HashMap<UserId, Arc<AtomicBool>>>>,
5071 forbid_connections: Arc<AtomicBool>,
5072 _test_db: TestDb,
5073}
5074
5075impl TestServer {
5076 async fn start(
5077 foreground: Rc<executor::Foreground>,
5078 background: Arc<executor::Background>,
5079 ) -> Self {
5080 let test_db = TestDb::fake(background.clone());
5081 let app_state = Self::build_app_state(&test_db).await;
5082 let peer = Peer::new();
5083 let notifications = mpsc::unbounded();
5084 let server = Server::new(app_state.clone(), Some(notifications.0));
5085 Self {
5086 peer,
5087 app_state,
5088 server,
5089 foreground,
5090 notifications: notifications.1,
5091 connection_killers: Default::default(),
5092 forbid_connections: Default::default(),
5093 _test_db: test_db,
5094 }
5095 }
5096
5097 async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
5098 cx.update(|cx| {
5099 let mut settings = Settings::test(cx);
5100 settings.projects_online_by_default = false;
5101 cx.set_global(settings);
5102 });
5103
5104 let http = FakeHttpClient::with_404_response();
5105 let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
5106 {
5107 user.id
5108 } else {
5109 self.app_state
5110 .db
5111 .create_user(name, None, false)
5112 .await
5113 .unwrap()
5114 };
5115 let client_name = name.to_string();
5116 let mut client = Client::new(http.clone());
5117 let server = self.server.clone();
5118 let db = self.app_state.db.clone();
5119 let connection_killers = self.connection_killers.clone();
5120 let forbid_connections = self.forbid_connections.clone();
5121 let (connection_id_tx, mut connection_id_rx) = mpsc::channel(16);
5122
5123 Arc::get_mut(&mut client)
5124 .unwrap()
5125 .set_id(user_id.0 as usize)
5126 .override_authenticate(move |cx| {
5127 cx.spawn(|_| async move {
5128 let access_token = "the-token".to_string();
5129 Ok(Credentials {
5130 user_id: user_id.0 as u64,
5131 access_token,
5132 })
5133 })
5134 })
5135 .override_establish_connection(move |credentials, cx| {
5136 assert_eq!(credentials.user_id, user_id.0 as u64);
5137 assert_eq!(credentials.access_token, "the-token");
5138
5139 let server = server.clone();
5140 let db = db.clone();
5141 let connection_killers = connection_killers.clone();
5142 let forbid_connections = forbid_connections.clone();
5143 let client_name = client_name.clone();
5144 let connection_id_tx = connection_id_tx.clone();
5145 cx.spawn(move |cx| async move {
5146 if forbid_connections.load(SeqCst) {
5147 Err(EstablishConnectionError::other(anyhow!(
5148 "server is forbidding connections"
5149 )))
5150 } else {
5151 let (client_conn, server_conn, killed) =
5152 Connection::in_memory(cx.background());
5153 connection_killers.lock().insert(user_id, killed);
5154 let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
5155 cx.background()
5156 .spawn(server.handle_connection(
5157 server_conn,
5158 client_name,
5159 user,
5160 Some(connection_id_tx),
5161 cx.background(),
5162 ))
5163 .detach();
5164 Ok(client_conn)
5165 }
5166 })
5167 });
5168
5169 let fs = FakeFs::new(cx.background());
5170 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
5171 let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake()));
5172 let app_state = Arc::new(workspace::AppState {
5173 client: client.clone(),
5174 user_store: user_store.clone(),
5175 project_store: project_store.clone(),
5176 languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
5177 themes: ThemeRegistry::new((), cx.font_cache()),
5178 fs: fs.clone(),
5179 build_window_options: Default::default,
5180 initialize_workspace: |_, _, _| unimplemented!(),
5181 });
5182
5183 Channel::init(&client);
5184 Project::init(&client);
5185 cx.update(|cx| workspace::init(app_state.clone(), cx));
5186
5187 client
5188 .authenticate_and_connect(false, &cx.to_async())
5189 .await
5190 .unwrap();
5191 let peer_id = PeerId(connection_id_rx.next().await.unwrap().0);
5192
5193 let client = TestClient {
5194 client,
5195 peer_id,
5196 username: name.to_string(),
5197 user_store,
5198 project_store,
5199 fs,
5200 language_registry: Arc::new(LanguageRegistry::test()),
5201 buffers: Default::default(),
5202 };
5203 client.wait_for_current_user(cx).await;
5204 client
5205 }
5206
5207 fn disconnect_client(&self, user_id: UserId) {
5208 self.connection_killers
5209 .lock()
5210 .remove(&user_id)
5211 .unwrap()
5212 .store(true, SeqCst);
5213 }
5214
5215 fn forbid_connections(&self) {
5216 self.forbid_connections.store(true, SeqCst);
5217 }
5218
5219 fn allow_connections(&self) {
5220 self.forbid_connections.store(false, SeqCst);
5221 }
5222
5223 async fn make_contacts(&self, mut clients: Vec<(&TestClient, &mut TestAppContext)>) {
5224 while let Some((client_a, cx_a)) = clients.pop() {
5225 for (client_b, cx_b) in &mut clients {
5226 client_a
5227 .user_store
5228 .update(cx_a, |store, cx| {
5229 store.request_contact(client_b.user_id().unwrap(), cx)
5230 })
5231 .await
5232 .unwrap();
5233 cx_a.foreground().run_until_parked();
5234 client_b
5235 .user_store
5236 .update(*cx_b, |store, cx| {
5237 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5238 })
5239 .await
5240 .unwrap();
5241 }
5242 }
5243 }
5244
5245 async fn build_app_state(test_db: &TestDb) -> Arc<AppState> {
5246 Arc::new(AppState {
5247 db: test_db.db().clone(),
5248 api_token: Default::default(),
5249 invite_link_prefix: Default::default(),
5250 })
5251 }
5252
5253 async fn condition<F>(&mut self, mut predicate: F)
5254 where
5255 F: FnMut(&Store) -> bool,
5256 {
5257 assert!(
5258 self.foreground.parking_forbidden(),
5259 "you must call forbid_parking to use server conditions so we don't block indefinitely"
5260 );
5261 while !(predicate)(&*self.server.store.lock().await) {
5262 self.foreground.start_waiting();
5263 self.notifications.next().await;
5264 self.foreground.finish_waiting();
5265 }
5266 }
5267}
5268
5269impl Deref for TestServer {
5270 type Target = Server;
5271
5272 fn deref(&self) -> &Self::Target {
5273 &self.server
5274 }
5275}
5276
5277impl Drop for TestServer {
5278 fn drop(&mut self) {
5279 self.peer.reset();
5280 }
5281}
5282
5283struct TestClient {
5284 client: Arc<Client>,
5285 username: String,
5286 pub peer_id: PeerId,
5287 pub user_store: ModelHandle<UserStore>,
5288 pub project_store: ModelHandle<ProjectStore>,
5289 language_registry: Arc<LanguageRegistry>,
5290 fs: Arc<FakeFs>,
5291 buffers: HashSet<ModelHandle<language::Buffer>>,
5292}
5293
5294impl Deref for TestClient {
5295 type Target = Arc<Client>;
5296
5297 fn deref(&self) -> &Self::Target {
5298 &self.client
5299 }
5300}
5301
5302struct ContactsSummary {
5303 pub current: Vec<String>,
5304 pub outgoing_requests: Vec<String>,
5305 pub incoming_requests: Vec<String>,
5306}
5307
5308impl TestClient {
5309 pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
5310 UserId::from_proto(
5311 self.user_store
5312 .read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
5313 )
5314 }
5315
5316 async fn wait_for_current_user(&self, cx: &TestAppContext) {
5317 let mut authed_user = self
5318 .user_store
5319 .read_with(cx, |user_store, _| user_store.watch_current_user());
5320 while authed_user.next().await.unwrap().is_none() {}
5321 }
5322
5323 async fn clear_contacts(&self, cx: &mut TestAppContext) {
5324 self.user_store
5325 .update(cx, |store, _| store.clear_contacts())
5326 .await;
5327 }
5328
5329 fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
5330 self.user_store.read_with(cx, |store, _| ContactsSummary {
5331 current: store
5332 .contacts()
5333 .iter()
5334 .map(|contact| contact.user.github_login.clone())
5335 .collect(),
5336 outgoing_requests: store
5337 .outgoing_contact_requests()
5338 .iter()
5339 .map(|user| user.github_login.clone())
5340 .collect(),
5341 incoming_requests: store
5342 .incoming_contact_requests()
5343 .iter()
5344 .map(|user| user.github_login.clone())
5345 .collect(),
5346 })
5347 }
5348
5349 async fn build_local_project(
5350 &self,
5351 root_path: impl AsRef<Path>,
5352 cx: &mut TestAppContext,
5353 ) -> (ModelHandle<Project>, WorktreeId) {
5354 let project = cx.update(|cx| {
5355 Project::local(
5356 true,
5357 self.client.clone(),
5358 self.user_store.clone(),
5359 self.project_store.clone(),
5360 self.language_registry.clone(),
5361 self.fs.clone(),
5362 cx,
5363 )
5364 });
5365 let (worktree, _) = project
5366 .update(cx, |p, cx| {
5367 p.find_or_create_local_worktree(root_path, true, cx)
5368 })
5369 .await
5370 .unwrap();
5371 worktree
5372 .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
5373 .await;
5374 project
5375 .update(cx, |project, _| project.next_remote_id())
5376 .await;
5377 (project, worktree.read_with(cx, |tree, _| tree.id()))
5378 }
5379
5380 async fn build_remote_project(
5381 &self,
5382 host_project: &ModelHandle<Project>,
5383 host_cx: &mut TestAppContext,
5384 guest_cx: &mut TestAppContext,
5385 ) -> ModelHandle<Project> {
5386 let host_project_id = host_project
5387 .read_with(host_cx, |project, _| project.next_remote_id())
5388 .await;
5389 let guest_user_id = self.user_id().unwrap();
5390 let languages = host_project.read_with(host_cx, |project, _| project.languages().clone());
5391 let project_b = guest_cx.spawn(|cx| {
5392 Project::remote(
5393 host_project_id,
5394 self.client.clone(),
5395 self.user_store.clone(),
5396 self.project_store.clone(),
5397 languages,
5398 FakeFs::new(cx.background()),
5399 cx,
5400 )
5401 });
5402 host_cx.foreground().run_until_parked();
5403 host_project.update(host_cx, |project, cx| {
5404 project.respond_to_join_request(guest_user_id, true, cx)
5405 });
5406 let project = project_b.await.unwrap();
5407 project
5408 }
5409
5410 fn build_workspace(
5411 &self,
5412 project: &ModelHandle<Project>,
5413 cx: &mut TestAppContext,
5414 ) -> ViewHandle<Workspace> {
5415 let (_, root_view) = cx.add_window(|_| EmptyView);
5416 cx.add_view(&root_view, |cx| Workspace::new(project.clone(), cx))
5417 }
5418
5419 async fn simulate_host(
5420 mut self,
5421 project: ModelHandle<Project>,
5422 op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5423 rng: Arc<Mutex<StdRng>>,
5424 mut cx: TestAppContext,
5425 ) -> (
5426 Self,
5427 ModelHandle<Project>,
5428 TestAppContext,
5429 Option<anyhow::Error>,
5430 ) {
5431 async fn simulate_host_internal(
5432 client: &mut TestClient,
5433 project: ModelHandle<Project>,
5434 mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5435 rng: Arc<Mutex<StdRng>>,
5436 cx: &mut TestAppContext,
5437 ) -> anyhow::Result<()> {
5438 let fs = project.read_with(cx, |project, _| project.fs().clone());
5439
5440 cx.update(|cx| {
5441 cx.subscribe(&project, move |project, event, cx| {
5442 if let project::Event::ContactRequestedJoin(user) = event {
5443 log::info!("Host: accepting join request from {}", user.github_login);
5444 project.update(cx, |project, cx| {
5445 project.respond_to_join_request(user.id, true, cx)
5446 });
5447 }
5448 })
5449 .detach();
5450 });
5451
5452 while op_start_signal.next().await.is_some() {
5453 let distribution = rng.lock().gen_range::<usize, _>(0..100);
5454 let files = fs.as_fake().files().await;
5455 match distribution {
5456 0..=19 if !files.is_empty() => {
5457 let path = files.choose(&mut *rng.lock()).unwrap();
5458 let mut path = path.as_path();
5459 while let Some(parent_path) = path.parent() {
5460 path = parent_path;
5461 if rng.lock().gen() {
5462 break;
5463 }
5464 }
5465
5466 log::info!("Host: find/create local worktree {:?}", path);
5467 let find_or_create_worktree = project.update(cx, |project, cx| {
5468 project.find_or_create_local_worktree(path, true, cx)
5469 });
5470 if rng.lock().gen() {
5471 cx.background().spawn(find_or_create_worktree).detach();
5472 } else {
5473 find_or_create_worktree.await?;
5474 }
5475 }
5476 20..=79 if !files.is_empty() => {
5477 let buffer = if client.buffers.is_empty() || rng.lock().gen() {
5478 let file = files.choose(&mut *rng.lock()).unwrap();
5479 let (worktree, path) = project
5480 .update(cx, |project, cx| {
5481 project.find_or_create_local_worktree(file.clone(), true, cx)
5482 })
5483 .await?;
5484 let project_path =
5485 worktree.read_with(cx, |worktree, _| (worktree.id(), path));
5486 log::info!(
5487 "Host: opening path {:?}, worktree {}, relative_path {:?}",
5488 file,
5489 project_path.0,
5490 project_path.1
5491 );
5492 let buffer = project
5493 .update(cx, |project, cx| project.open_buffer(project_path, cx))
5494 .await
5495 .unwrap();
5496 client.buffers.insert(buffer.clone());
5497 buffer
5498 } else {
5499 client
5500 .buffers
5501 .iter()
5502 .choose(&mut *rng.lock())
5503 .unwrap()
5504 .clone()
5505 };
5506
5507 if rng.lock().gen_bool(0.1) {
5508 cx.update(|cx| {
5509 log::info!(
5510 "Host: dropping buffer {:?}",
5511 buffer.read(cx).file().unwrap().full_path(cx)
5512 );
5513 client.buffers.remove(&buffer);
5514 drop(buffer);
5515 });
5516 } else {
5517 buffer.update(cx, |buffer, cx| {
5518 log::info!(
5519 "Host: updating buffer {:?} ({})",
5520 buffer.file().unwrap().full_path(cx),
5521 buffer.remote_id()
5522 );
5523
5524 if rng.lock().gen_bool(0.7) {
5525 buffer.randomly_edit(&mut *rng.lock(), 5, cx);
5526 } else {
5527 buffer.randomly_undo_redo(&mut *rng.lock(), cx);
5528 }
5529 });
5530 }
5531 }
5532 _ => loop {
5533 let path_component_count = rng.lock().gen_range::<usize, _>(1..=5);
5534 let mut path = PathBuf::new();
5535 path.push("/");
5536 for _ in 0..path_component_count {
5537 let letter = rng.lock().gen_range(b'a'..=b'z');
5538 path.push(std::str::from_utf8(&[letter]).unwrap());
5539 }
5540 path.set_extension("rs");
5541 let parent_path = path.parent().unwrap();
5542
5543 log::info!("Host: creating file {:?}", path,);
5544
5545 if fs.create_dir(parent_path).await.is_ok()
5546 && fs.create_file(&path, Default::default()).await.is_ok()
5547 {
5548 break;
5549 } else {
5550 log::info!("Host: cannot create file");
5551 }
5552 },
5553 }
5554
5555 cx.background().simulate_random_delay().await;
5556 }
5557
5558 Ok(())
5559 }
5560
5561 let result =
5562 simulate_host_internal(&mut self, project.clone(), op_start_signal, rng, &mut cx).await;
5563 log::info!("Host done");
5564 (self, project, cx, result.err())
5565 }
5566
5567 pub async fn simulate_guest(
5568 mut self,
5569 guest_username: String,
5570 project: ModelHandle<Project>,
5571 op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5572 rng: Arc<Mutex<StdRng>>,
5573 mut cx: TestAppContext,
5574 ) -> (
5575 Self,
5576 ModelHandle<Project>,
5577 TestAppContext,
5578 Option<anyhow::Error>,
5579 ) {
5580 async fn simulate_guest_internal(
5581 client: &mut TestClient,
5582 guest_username: &str,
5583 project: ModelHandle<Project>,
5584 mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5585 rng: Arc<Mutex<StdRng>>,
5586 cx: &mut TestAppContext,
5587 ) -> anyhow::Result<()> {
5588 while op_start_signal.next().await.is_some() {
5589 let buffer = if client.buffers.is_empty() || rng.lock().gen() {
5590 let worktree = if let Some(worktree) = project.read_with(cx, |project, cx| {
5591 project
5592 .worktrees(cx)
5593 .filter(|worktree| {
5594 let worktree = worktree.read(cx);
5595 worktree.is_visible()
5596 && worktree.entries(false).any(|e| e.is_file())
5597 })
5598 .choose(&mut *rng.lock())
5599 }) {
5600 worktree
5601 } else {
5602 cx.background().simulate_random_delay().await;
5603 continue;
5604 };
5605
5606 let (worktree_root_name, project_path) =
5607 worktree.read_with(cx, |worktree, _| {
5608 let entry = worktree
5609 .entries(false)
5610 .filter(|e| e.is_file())
5611 .choose(&mut *rng.lock())
5612 .unwrap();
5613 (
5614 worktree.root_name().to_string(),
5615 (worktree.id(), entry.path.clone()),
5616 )
5617 });
5618 log::info!(
5619 "{}: opening path {:?} in worktree {} ({})",
5620 guest_username,
5621 project_path.1,
5622 project_path.0,
5623 worktree_root_name,
5624 );
5625 let buffer = project
5626 .update(cx, |project, cx| {
5627 project.open_buffer(project_path.clone(), cx)
5628 })
5629 .await?;
5630 log::info!(
5631 "{}: opened path {:?} in worktree {} ({}) with buffer id {}",
5632 guest_username,
5633 project_path.1,
5634 project_path.0,
5635 worktree_root_name,
5636 buffer.read_with(cx, |buffer, _| buffer.remote_id())
5637 );
5638 client.buffers.insert(buffer.clone());
5639 buffer
5640 } else {
5641 client
5642 .buffers
5643 .iter()
5644 .choose(&mut *rng.lock())
5645 .unwrap()
5646 .clone()
5647 };
5648
5649 let choice = rng.lock().gen_range(0..100);
5650 match choice {
5651 0..=9 => {
5652 cx.update(|cx| {
5653 log::info!(
5654 "{}: dropping buffer {:?}",
5655 guest_username,
5656 buffer.read(cx).file().unwrap().full_path(cx)
5657 );
5658 client.buffers.remove(&buffer);
5659 drop(buffer);
5660 });
5661 }
5662 10..=19 => {
5663 let completions = project.update(cx, |project, cx| {
5664 log::info!(
5665 "{}: requesting completions for buffer {} ({:?})",
5666 guest_username,
5667 buffer.read(cx).remote_id(),
5668 buffer.read(cx).file().unwrap().full_path(cx)
5669 );
5670 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5671 project.completions(&buffer, offset, cx)
5672 });
5673 let completions = cx.background().spawn(async move {
5674 completions
5675 .await
5676 .map_err(|err| anyhow!("completions request failed: {:?}", err))
5677 });
5678 if rng.lock().gen_bool(0.3) {
5679 log::info!("{}: detaching completions request", guest_username);
5680 cx.update(|cx| completions.detach_and_log_err(cx));
5681 } else {
5682 completions.await?;
5683 }
5684 }
5685 20..=29 => {
5686 let code_actions = project.update(cx, |project, cx| {
5687 log::info!(
5688 "{}: requesting code actions for buffer {} ({:?})",
5689 guest_username,
5690 buffer.read(cx).remote_id(),
5691 buffer.read(cx).file().unwrap().full_path(cx)
5692 );
5693 let range = buffer.read(cx).random_byte_range(0, &mut *rng.lock());
5694 project.code_actions(&buffer, range, cx)
5695 });
5696 let code_actions = cx.background().spawn(async move {
5697 code_actions
5698 .await
5699 .map_err(|err| anyhow!("code actions request failed: {:?}", err))
5700 });
5701 if rng.lock().gen_bool(0.3) {
5702 log::info!("{}: detaching code actions request", guest_username);
5703 cx.update(|cx| code_actions.detach_and_log_err(cx));
5704 } else {
5705 code_actions.await?;
5706 }
5707 }
5708 30..=39 if buffer.read_with(cx, |buffer, _| buffer.is_dirty()) => {
5709 let (requested_version, save) = buffer.update(cx, |buffer, cx| {
5710 log::info!(
5711 "{}: saving buffer {} ({:?})",
5712 guest_username,
5713 buffer.remote_id(),
5714 buffer.file().unwrap().full_path(cx)
5715 );
5716 (buffer.version(), buffer.save(cx))
5717 });
5718 let save = cx.background().spawn(async move {
5719 let (saved_version, _, _) = save
5720 .await
5721 .map_err(|err| anyhow!("save request failed: {:?}", err))?;
5722 assert!(saved_version.observed_all(&requested_version));
5723 Ok::<_, anyhow::Error>(())
5724 });
5725 if rng.lock().gen_bool(0.3) {
5726 log::info!("{}: detaching save request", guest_username);
5727 cx.update(|cx| save.detach_and_log_err(cx));
5728 } else {
5729 save.await?;
5730 }
5731 }
5732 40..=44 => {
5733 let prepare_rename = project.update(cx, |project, cx| {
5734 log::info!(
5735 "{}: preparing rename for buffer {} ({:?})",
5736 guest_username,
5737 buffer.read(cx).remote_id(),
5738 buffer.read(cx).file().unwrap().full_path(cx)
5739 );
5740 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5741 project.prepare_rename(buffer, offset, cx)
5742 });
5743 let prepare_rename = cx.background().spawn(async move {
5744 prepare_rename
5745 .await
5746 .map_err(|err| anyhow!("prepare rename request failed: {:?}", err))
5747 });
5748 if rng.lock().gen_bool(0.3) {
5749 log::info!("{}: detaching prepare rename request", guest_username);
5750 cx.update(|cx| prepare_rename.detach_and_log_err(cx));
5751 } else {
5752 prepare_rename.await?;
5753 }
5754 }
5755 45..=49 => {
5756 let definitions = project.update(cx, |project, cx| {
5757 log::info!(
5758 "{}: requesting definitions for buffer {} ({:?})",
5759 guest_username,
5760 buffer.read(cx).remote_id(),
5761 buffer.read(cx).file().unwrap().full_path(cx)
5762 );
5763 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5764 project.definition(&buffer, offset, cx)
5765 });
5766 let definitions = cx.background().spawn(async move {
5767 definitions
5768 .await
5769 .map_err(|err| anyhow!("definitions request failed: {:?}", err))
5770 });
5771 if rng.lock().gen_bool(0.3) {
5772 log::info!("{}: detaching definitions request", guest_username);
5773 cx.update(|cx| definitions.detach_and_log_err(cx));
5774 } else {
5775 client.buffers.extend(
5776 definitions.await?.into_iter().map(|loc| loc.target.buffer),
5777 );
5778 }
5779 }
5780 50..=54 => {
5781 let highlights = project.update(cx, |project, cx| {
5782 log::info!(
5783 "{}: requesting highlights for buffer {} ({:?})",
5784 guest_username,
5785 buffer.read(cx).remote_id(),
5786 buffer.read(cx).file().unwrap().full_path(cx)
5787 );
5788 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5789 project.document_highlights(&buffer, offset, cx)
5790 });
5791 let highlights = cx.background().spawn(async move {
5792 highlights
5793 .await
5794 .map_err(|err| anyhow!("highlights request failed: {:?}", err))
5795 });
5796 if rng.lock().gen_bool(0.3) {
5797 log::info!("{}: detaching highlights request", guest_username);
5798 cx.update(|cx| highlights.detach_and_log_err(cx));
5799 } else {
5800 highlights.await?;
5801 }
5802 }
5803 55..=59 => {
5804 let search = project.update(cx, |project, cx| {
5805 let query = rng.lock().gen_range('a'..='z');
5806 log::info!("{}: project-wide search {:?}", guest_username, query);
5807 project.search(SearchQuery::text(query, false, false), cx)
5808 });
5809 let search = cx.background().spawn(async move {
5810 search
5811 .await
5812 .map_err(|err| anyhow!("search request failed: {:?}", err))
5813 });
5814 if rng.lock().gen_bool(0.3) {
5815 log::info!("{}: detaching search request", guest_username);
5816 cx.update(|cx| search.detach_and_log_err(cx));
5817 } else {
5818 client.buffers.extend(search.await?.into_keys());
5819 }
5820 }
5821 60..=69 => {
5822 let worktree = project
5823 .read_with(cx, |project, cx| {
5824 project
5825 .worktrees(cx)
5826 .filter(|worktree| {
5827 let worktree = worktree.read(cx);
5828 worktree.is_visible()
5829 && worktree.entries(false).any(|e| e.is_file())
5830 && worktree.root_entry().map_or(false, |e| e.is_dir())
5831 })
5832 .choose(&mut *rng.lock())
5833 })
5834 .unwrap();
5835 let (worktree_id, worktree_root_name) = worktree
5836 .read_with(cx, |worktree, _| {
5837 (worktree.id(), worktree.root_name().to_string())
5838 });
5839
5840 let mut new_name = String::new();
5841 for _ in 0..10 {
5842 let letter = rng.lock().gen_range('a'..='z');
5843 new_name.push(letter);
5844 }
5845 let mut new_path = PathBuf::new();
5846 new_path.push(new_name);
5847 new_path.set_extension("rs");
5848 log::info!(
5849 "{}: creating {:?} in worktree {} ({})",
5850 guest_username,
5851 new_path,
5852 worktree_id,
5853 worktree_root_name,
5854 );
5855 project
5856 .update(cx, |project, cx| {
5857 project.create_entry((worktree_id, new_path), false, cx)
5858 })
5859 .unwrap()
5860 .await?;
5861 }
5862 _ => {
5863 buffer.update(cx, |buffer, cx| {
5864 log::info!(
5865 "{}: updating buffer {} ({:?})",
5866 guest_username,
5867 buffer.remote_id(),
5868 buffer.file().unwrap().full_path(cx)
5869 );
5870 if rng.lock().gen_bool(0.7) {
5871 buffer.randomly_edit(&mut *rng.lock(), 5, cx);
5872 } else {
5873 buffer.randomly_undo_redo(&mut *rng.lock(), cx);
5874 }
5875 });
5876 }
5877 }
5878 cx.background().simulate_random_delay().await;
5879 }
5880 Ok(())
5881 }
5882
5883 let result = simulate_guest_internal(
5884 &mut self,
5885 &guest_username,
5886 project.clone(),
5887 op_start_signal,
5888 rng,
5889 &mut cx,
5890 )
5891 .await;
5892 log::info!("{}: done", guest_username);
5893
5894 (self, project, cx, result.err())
5895 }
5896}
5897
5898impl Drop for TestClient {
5899 fn drop(&mut self) {
5900 self.client.tear_down();
5901 }
5902}
5903
5904impl Executor for Arc<gpui::executor::Background> {
5905 type Sleep = gpui::executor::Timer;
5906
5907 fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F) {
5908 self.spawn(future).detach();
5909 }
5910
5911 fn sleep(&self, duration: Duration) -> Self::Sleep {
5912 self.as_ref().timer(duration)
5913 }
5914}
5915
5916fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> {
5917 channel
5918 .messages()
5919 .cursor::<()>()
5920 .map(|m| {
5921 (
5922 m.sender.github_login.clone(),
5923 m.body.clone(),
5924 m.is_pending(),
5925 )
5926 })
5927 .collect()
5928}