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