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