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