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