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