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