1use crate::{
2 Entry, EntryKind, Event, PathChange, WorkDirectory, Worktree, WorktreeModelHandle,
3 worktree_settings::WorktreeSettings,
4};
5use anyhow::Result;
6use fs::{FakeFs, Fs, RealFs, RemoveOptions};
7use git::GITIGNORE;
8use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext};
9use parking_lot::Mutex;
10use postage::stream::Stream;
11use pretty_assertions::assert_eq;
12use rand::prelude::*;
13
14use serde_json::json;
15use settings::{Settings, SettingsStore};
16use std::{
17 env,
18 fmt::Write,
19 mem,
20 path::{Path, PathBuf},
21 sync::Arc,
22};
23use util::{ResultExt, path, test::TempTree};
24
25#[gpui::test]
26async fn test_traversal(cx: &mut TestAppContext) {
27 init_test(cx);
28 let fs = FakeFs::new(cx.background_executor.clone());
29 fs.insert_tree(
30 "/root",
31 json!({
32 ".gitignore": "a/b\n",
33 "a": {
34 "b": "",
35 "c": "",
36 }
37 }),
38 )
39 .await;
40
41 let tree = Worktree::local(
42 Path::new("/root"),
43 true,
44 fs,
45 Default::default(),
46 &mut cx.to_async(),
47 )
48 .await
49 .unwrap();
50 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
51 .await;
52
53 tree.read_with(cx, |tree, _| {
54 assert_eq!(
55 tree.entries(false, 0)
56 .map(|entry| entry.path.as_ref())
57 .collect::<Vec<_>>(),
58 vec![
59 Path::new(""),
60 Path::new(".gitignore"),
61 Path::new("a"),
62 Path::new("a/c"),
63 ]
64 );
65 assert_eq!(
66 tree.entries(true, 0)
67 .map(|entry| entry.path.as_ref())
68 .collect::<Vec<_>>(),
69 vec![
70 Path::new(""),
71 Path::new(".gitignore"),
72 Path::new("a"),
73 Path::new("a/b"),
74 Path::new("a/c"),
75 ]
76 );
77 })
78}
79
80#[gpui::test(iterations = 10)]
81async fn test_circular_symlinks(cx: &mut TestAppContext) {
82 init_test(cx);
83 let fs = FakeFs::new(cx.background_executor.clone());
84 fs.insert_tree(
85 "/root",
86 json!({
87 "lib": {
88 "a": {
89 "a.txt": ""
90 },
91 "b": {
92 "b.txt": ""
93 }
94 }
95 }),
96 )
97 .await;
98 fs.create_symlink("/root/lib/a/lib".as_ref(), "..".into())
99 .await
100 .unwrap();
101 fs.create_symlink("/root/lib/b/lib".as_ref(), "..".into())
102 .await
103 .unwrap();
104
105 let tree = Worktree::local(
106 Path::new("/root"),
107 true,
108 fs.clone(),
109 Default::default(),
110 &mut cx.to_async(),
111 )
112 .await
113 .unwrap();
114
115 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
116 .await;
117
118 tree.read_with(cx, |tree, _| {
119 assert_eq!(
120 tree.entries(false, 0)
121 .map(|entry| entry.path.as_ref())
122 .collect::<Vec<_>>(),
123 vec![
124 Path::new(""),
125 Path::new("lib"),
126 Path::new("lib/a"),
127 Path::new("lib/a/a.txt"),
128 Path::new("lib/a/lib"),
129 Path::new("lib/b"),
130 Path::new("lib/b/b.txt"),
131 Path::new("lib/b/lib"),
132 ]
133 );
134 });
135
136 fs.rename(
137 Path::new("/root/lib/a/lib"),
138 Path::new("/root/lib/a/lib-2"),
139 Default::default(),
140 )
141 .await
142 .unwrap();
143 cx.executor().run_until_parked();
144 tree.read_with(cx, |tree, _| {
145 assert_eq!(
146 tree.entries(false, 0)
147 .map(|entry| entry.path.as_ref())
148 .collect::<Vec<_>>(),
149 vec![
150 Path::new(""),
151 Path::new("lib"),
152 Path::new("lib/a"),
153 Path::new("lib/a/a.txt"),
154 Path::new("lib/a/lib-2"),
155 Path::new("lib/b"),
156 Path::new("lib/b/b.txt"),
157 Path::new("lib/b/lib"),
158 ]
159 );
160 });
161}
162
163#[gpui::test]
164async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
165 init_test(cx);
166 let fs = FakeFs::new(cx.background_executor.clone());
167 fs.insert_tree(
168 "/root",
169 json!({
170 "dir1": {
171 "deps": {
172 // symlinks here
173 },
174 "src": {
175 "a.rs": "",
176 "b.rs": "",
177 },
178 },
179 "dir2": {
180 "src": {
181 "c.rs": "",
182 "d.rs": "",
183 }
184 },
185 "dir3": {
186 "deps": {},
187 "src": {
188 "e.rs": "",
189 "f.rs": "",
190 },
191 }
192 }),
193 )
194 .await;
195
196 // These symlinks point to directories outside of the worktree's root, dir1.
197 fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into())
198 .await
199 .unwrap();
200 fs.create_symlink("/root/dir1/deps/dep-dir3".as_ref(), "../../dir3".into())
201 .await
202 .unwrap();
203
204 let tree = Worktree::local(
205 Path::new("/root/dir1"),
206 true,
207 fs.clone(),
208 Default::default(),
209 &mut cx.to_async(),
210 )
211 .await
212 .unwrap();
213
214 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
215 .await;
216
217 let tree_updates = Arc::new(Mutex::new(Vec::new()));
218 tree.update(cx, |_, cx| {
219 let tree_updates = tree_updates.clone();
220 cx.subscribe(&tree, move |_, _, event, _| {
221 if let Event::UpdatedEntries(update) = event {
222 tree_updates.lock().extend(
223 update
224 .iter()
225 .map(|(path, _, change)| (path.clone(), *change)),
226 );
227 }
228 })
229 .detach();
230 });
231
232 // The symlinked directories are not scanned by default.
233 tree.read_with(cx, |tree, _| {
234 assert_eq!(
235 tree.entries(true, 0)
236 .map(|entry| (entry.path.as_ref(), entry.is_external))
237 .collect::<Vec<_>>(),
238 vec![
239 (Path::new(""), false),
240 (Path::new("deps"), false),
241 (Path::new("deps/dep-dir2"), true),
242 (Path::new("deps/dep-dir3"), true),
243 (Path::new("src"), false),
244 (Path::new("src/a.rs"), false),
245 (Path::new("src/b.rs"), false),
246 ]
247 );
248
249 assert_eq!(
250 tree.entry_for_path("deps/dep-dir2").unwrap().kind,
251 EntryKind::UnloadedDir
252 );
253 });
254
255 // Expand one of the symlinked directories.
256 tree.read_with(cx, |tree, _| {
257 tree.as_local()
258 .unwrap()
259 .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()])
260 })
261 .recv()
262 .await;
263
264 // The expanded directory's contents are loaded. Subdirectories are
265 // not scanned yet.
266 tree.read_with(cx, |tree, _| {
267 assert_eq!(
268 tree.entries(true, 0)
269 .map(|entry| (entry.path.as_ref(), entry.is_external))
270 .collect::<Vec<_>>(),
271 vec![
272 (Path::new(""), false),
273 (Path::new("deps"), false),
274 (Path::new("deps/dep-dir2"), true),
275 (Path::new("deps/dep-dir3"), true),
276 (Path::new("deps/dep-dir3/deps"), true),
277 (Path::new("deps/dep-dir3/src"), true),
278 (Path::new("src"), false),
279 (Path::new("src/a.rs"), false),
280 (Path::new("src/b.rs"), false),
281 ]
282 );
283 });
284 assert_eq!(
285 mem::take(&mut *tree_updates.lock()),
286 &[
287 (Path::new("deps/dep-dir3").into(), PathChange::Loaded),
288 (Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded),
289 (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded)
290 ]
291 );
292
293 // Expand a subdirectory of one of the symlinked directories.
294 tree.read_with(cx, |tree, _| {
295 tree.as_local()
296 .unwrap()
297 .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()])
298 })
299 .recv()
300 .await;
301
302 // The expanded subdirectory's contents are loaded.
303 tree.read_with(cx, |tree, _| {
304 assert_eq!(
305 tree.entries(true, 0)
306 .map(|entry| (entry.path.as_ref(), entry.is_external))
307 .collect::<Vec<_>>(),
308 vec![
309 (Path::new(""), false),
310 (Path::new("deps"), false),
311 (Path::new("deps/dep-dir2"), true),
312 (Path::new("deps/dep-dir3"), true),
313 (Path::new("deps/dep-dir3/deps"), true),
314 (Path::new("deps/dep-dir3/src"), true),
315 (Path::new("deps/dep-dir3/src/e.rs"), true),
316 (Path::new("deps/dep-dir3/src/f.rs"), true),
317 (Path::new("src"), false),
318 (Path::new("src/a.rs"), false),
319 (Path::new("src/b.rs"), false),
320 ]
321 );
322 });
323
324 assert_eq!(
325 mem::take(&mut *tree_updates.lock()),
326 &[
327 (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded),
328 (
329 Path::new("deps/dep-dir3/src/e.rs").into(),
330 PathChange::Loaded
331 ),
332 (
333 Path::new("deps/dep-dir3/src/f.rs").into(),
334 PathChange::Loaded
335 )
336 ]
337 );
338}
339
340#[cfg(target_os = "macos")]
341#[gpui::test]
342async fn test_renaming_case_only(cx: &mut TestAppContext) {
343 cx.executor().allow_parking();
344 init_test(cx);
345
346 const OLD_NAME: &str = "aaa.rs";
347 const NEW_NAME: &str = "AAA.rs";
348
349 let fs = Arc::new(RealFs::new(None, cx.executor()));
350 let temp_root = TempTree::new(json!({
351 OLD_NAME: "",
352 }));
353
354 let tree = Worktree::local(
355 temp_root.path(),
356 true,
357 fs.clone(),
358 Default::default(),
359 &mut cx.to_async(),
360 )
361 .await
362 .unwrap();
363
364 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
365 .await;
366 tree.read_with(cx, |tree, _| {
367 assert_eq!(
368 tree.entries(true, 0)
369 .map(|entry| entry.path.as_ref())
370 .collect::<Vec<_>>(),
371 vec![Path::new(""), Path::new(OLD_NAME)]
372 );
373 });
374
375 fs.rename(
376 &temp_root.path().join(OLD_NAME),
377 &temp_root.path().join(NEW_NAME),
378 fs::RenameOptions {
379 overwrite: true,
380 ignore_if_exists: true,
381 },
382 )
383 .await
384 .unwrap();
385
386 tree.flush_fs_events(cx).await;
387
388 tree.read_with(cx, |tree, _| {
389 assert_eq!(
390 tree.entries(true, 0)
391 .map(|entry| entry.path.as_ref())
392 .collect::<Vec<_>>(),
393 vec![Path::new(""), Path::new(NEW_NAME)]
394 );
395 });
396}
397
398#[gpui::test]
399async fn test_open_gitignored_files(cx: &mut TestAppContext) {
400 init_test(cx);
401 let fs = FakeFs::new(cx.background_executor.clone());
402 fs.insert_tree(
403 "/root",
404 json!({
405 ".gitignore": "node_modules\n",
406 "one": {
407 "node_modules": {
408 "a": {
409 "a1.js": "a1",
410 "a2.js": "a2",
411 },
412 "b": {
413 "b1.js": "b1",
414 "b2.js": "b2",
415 },
416 "c": {
417 "c1.js": "c1",
418 "c2.js": "c2",
419 }
420 },
421 },
422 "two": {
423 "x.js": "",
424 "y.js": "",
425 },
426 }),
427 )
428 .await;
429
430 let tree = Worktree::local(
431 Path::new("/root"),
432 true,
433 fs.clone(),
434 Default::default(),
435 &mut cx.to_async(),
436 )
437 .await
438 .unwrap();
439
440 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
441 .await;
442
443 tree.read_with(cx, |tree, _| {
444 assert_eq!(
445 tree.entries(true, 0)
446 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
447 .collect::<Vec<_>>(),
448 vec![
449 (Path::new(""), false),
450 (Path::new(".gitignore"), false),
451 (Path::new("one"), false),
452 (Path::new("one/node_modules"), true),
453 (Path::new("two"), false),
454 (Path::new("two/x.js"), false),
455 (Path::new("two/y.js"), false),
456 ]
457 );
458 });
459
460 // Open a file that is nested inside of a gitignored directory that
461 // has not yet been expanded.
462 let prev_read_dir_count = fs.read_dir_call_count();
463 let loaded = tree
464 .update(cx, |tree, cx| {
465 tree.load_file("one/node_modules/b/b1.js".as_ref(), cx)
466 })
467 .await
468 .unwrap();
469
470 tree.read_with(cx, |tree, _| {
471 assert_eq!(
472 tree.entries(true, 0)
473 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
474 .collect::<Vec<_>>(),
475 vec![
476 (Path::new(""), false),
477 (Path::new(".gitignore"), false),
478 (Path::new("one"), false),
479 (Path::new("one/node_modules"), true),
480 (Path::new("one/node_modules/a"), true),
481 (Path::new("one/node_modules/b"), true),
482 (Path::new("one/node_modules/b/b1.js"), true),
483 (Path::new("one/node_modules/b/b2.js"), true),
484 (Path::new("one/node_modules/c"), true),
485 (Path::new("two"), false),
486 (Path::new("two/x.js"), false),
487 (Path::new("two/y.js"), false),
488 ]
489 );
490
491 assert_eq!(
492 loaded.file.path.as_ref(),
493 Path::new("one/node_modules/b/b1.js")
494 );
495
496 // Only the newly-expanded directories are scanned.
497 assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2);
498 });
499
500 // Open another file in a different subdirectory of the same
501 // gitignored directory.
502 let prev_read_dir_count = fs.read_dir_call_count();
503 let loaded = tree
504 .update(cx, |tree, cx| {
505 tree.load_file("one/node_modules/a/a2.js".as_ref(), cx)
506 })
507 .await
508 .unwrap();
509
510 tree.read_with(cx, |tree, _| {
511 assert_eq!(
512 tree.entries(true, 0)
513 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
514 .collect::<Vec<_>>(),
515 vec![
516 (Path::new(""), false),
517 (Path::new(".gitignore"), false),
518 (Path::new("one"), false),
519 (Path::new("one/node_modules"), true),
520 (Path::new("one/node_modules/a"), true),
521 (Path::new("one/node_modules/a/a1.js"), true),
522 (Path::new("one/node_modules/a/a2.js"), true),
523 (Path::new("one/node_modules/b"), true),
524 (Path::new("one/node_modules/b/b1.js"), true),
525 (Path::new("one/node_modules/b/b2.js"), true),
526 (Path::new("one/node_modules/c"), true),
527 (Path::new("two"), false),
528 (Path::new("two/x.js"), false),
529 (Path::new("two/y.js"), false),
530 ]
531 );
532
533 assert_eq!(
534 loaded.file.path.as_ref(),
535 Path::new("one/node_modules/a/a2.js")
536 );
537
538 // Only the newly-expanded directory is scanned.
539 assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
540 });
541
542 let path = PathBuf::from("/root/one/node_modules/c/lib");
543
544 // No work happens when files and directories change within an unloaded directory.
545 let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count();
546 // When we open a directory, we check each ancestor whether it's a git
547 // repository. That means we have an fs.metadata call per ancestor that we
548 // need to subtract here.
549 let ancestors = path.ancestors().count();
550
551 fs.create_dir(path.as_ref()).await.unwrap();
552 cx.executor().run_until_parked();
553
554 assert_eq!(
555 fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count - ancestors,
556 0
557 );
558}
559
560#[gpui::test]
561async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
562 init_test(cx);
563 let fs = FakeFs::new(cx.background_executor.clone());
564 fs.insert_tree(
565 "/root",
566 json!({
567 ".gitignore": "node_modules\n",
568 "a": {
569 "a.js": "",
570 },
571 "b": {
572 "b.js": "",
573 },
574 "node_modules": {
575 "c": {
576 "c.js": "",
577 },
578 "d": {
579 "d.js": "",
580 "e": {
581 "e1.js": "",
582 "e2.js": "",
583 },
584 "f": {
585 "f1.js": "",
586 "f2.js": "",
587 }
588 },
589 },
590 }),
591 )
592 .await;
593
594 let tree = Worktree::local(
595 Path::new("/root"),
596 true,
597 fs.clone(),
598 Default::default(),
599 &mut cx.to_async(),
600 )
601 .await
602 .unwrap();
603
604 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
605 .await;
606
607 // Open a file within the gitignored directory, forcing some of its
608 // subdirectories to be read, but not all.
609 let read_dir_count_1 = fs.read_dir_call_count();
610 tree.read_with(cx, |tree, _| {
611 tree.as_local()
612 .unwrap()
613 .refresh_entries_for_paths(vec![Path::new("node_modules/d/d.js").into()])
614 })
615 .recv()
616 .await;
617
618 // Those subdirectories are now loaded.
619 tree.read_with(cx, |tree, _| {
620 assert_eq!(
621 tree.entries(true, 0)
622 .map(|e| (e.path.as_ref(), e.is_ignored))
623 .collect::<Vec<_>>(),
624 &[
625 (Path::new(""), false),
626 (Path::new(".gitignore"), false),
627 (Path::new("a"), false),
628 (Path::new("a/a.js"), false),
629 (Path::new("b"), false),
630 (Path::new("b/b.js"), false),
631 (Path::new("node_modules"), true),
632 (Path::new("node_modules/c"), true),
633 (Path::new("node_modules/d"), true),
634 (Path::new("node_modules/d/d.js"), true),
635 (Path::new("node_modules/d/e"), true),
636 (Path::new("node_modules/d/f"), true),
637 ]
638 );
639 });
640 let read_dir_count_2 = fs.read_dir_call_count();
641 assert_eq!(read_dir_count_2 - read_dir_count_1, 2);
642
643 // Update the gitignore so that node_modules is no longer ignored,
644 // but a subdirectory is ignored
645 fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default())
646 .await
647 .unwrap();
648 cx.executor().run_until_parked();
649
650 // All of the directories that are no longer ignored are now loaded.
651 tree.read_with(cx, |tree, _| {
652 assert_eq!(
653 tree.entries(true, 0)
654 .map(|e| (e.path.as_ref(), e.is_ignored))
655 .collect::<Vec<_>>(),
656 &[
657 (Path::new(""), false),
658 (Path::new(".gitignore"), false),
659 (Path::new("a"), false),
660 (Path::new("a/a.js"), false),
661 (Path::new("b"), false),
662 (Path::new("b/b.js"), false),
663 // This directory is no longer ignored
664 (Path::new("node_modules"), false),
665 (Path::new("node_modules/c"), false),
666 (Path::new("node_modules/c/c.js"), false),
667 (Path::new("node_modules/d"), false),
668 (Path::new("node_modules/d/d.js"), false),
669 // This subdirectory is now ignored
670 (Path::new("node_modules/d/e"), true),
671 (Path::new("node_modules/d/f"), false),
672 (Path::new("node_modules/d/f/f1.js"), false),
673 (Path::new("node_modules/d/f/f2.js"), false),
674 ]
675 );
676 });
677
678 // Each of the newly-loaded directories is scanned only once.
679 let read_dir_count_3 = fs.read_dir_call_count();
680 assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
681}
682
683#[gpui::test]
684async fn test_write_file(cx: &mut TestAppContext) {
685 init_test(cx);
686 cx.executor().allow_parking();
687 let dir = TempTree::new(json!({
688 ".git": {},
689 ".gitignore": "ignored-dir\n",
690 "tracked-dir": {},
691 "ignored-dir": {}
692 }));
693
694 let worktree = Worktree::local(
695 dir.path(),
696 true,
697 Arc::new(RealFs::new(None, cx.executor())),
698 Default::default(),
699 &mut cx.to_async(),
700 )
701 .await
702 .unwrap();
703
704 #[cfg(not(target_os = "macos"))]
705 fs::fs_watcher::global(|_| {}).unwrap();
706
707 cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
708 .await;
709 worktree.flush_fs_events(cx).await;
710
711 worktree
712 .update(cx, |tree, cx| {
713 tree.write_file(
714 Path::new("tracked-dir/file.txt"),
715 "hello".into(),
716 Default::default(),
717 cx,
718 )
719 })
720 .await
721 .unwrap();
722 worktree
723 .update(cx, |tree, cx| {
724 tree.write_file(
725 Path::new("ignored-dir/file.txt"),
726 "world".into(),
727 Default::default(),
728 cx,
729 )
730 })
731 .await
732 .unwrap();
733
734 worktree.read_with(cx, |tree, _| {
735 let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
736 let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
737 assert!(!tracked.is_ignored);
738 assert!(ignored.is_ignored);
739 });
740}
741
742#[gpui::test]
743async fn test_file_scan_inclusions(cx: &mut TestAppContext) {
744 init_test(cx);
745 cx.executor().allow_parking();
746 let dir = TempTree::new(json!({
747 ".gitignore": "**/target\n/node_modules\ntop_level.txt\n",
748 "target": {
749 "index": "blah2"
750 },
751 "node_modules": {
752 ".DS_Store": "",
753 "prettier": {
754 "package.json": "{}",
755 },
756 },
757 "src": {
758 ".DS_Store": "",
759 "foo": {
760 "foo.rs": "mod another;\n",
761 "another.rs": "// another",
762 },
763 "bar": {
764 "bar.rs": "// bar",
765 },
766 "lib.rs": "mod foo;\nmod bar;\n",
767 },
768 "top_level.txt": "top level file",
769 ".DS_Store": "",
770 }));
771 cx.update(|cx| {
772 cx.update_global::<SettingsStore, _>(|store, cx| {
773 store.update_user_settings(cx, |settings| {
774 settings.project.worktree.file_scan_exclusions = Some(vec![]);
775 settings.project.worktree.file_scan_inclusions = Some(vec![
776 "node_modules/**/package.json".to_string(),
777 "**/.DS_Store".to_string(),
778 ]);
779 });
780 });
781 });
782
783 let tree = Worktree::local(
784 dir.path(),
785 true,
786 Arc::new(RealFs::new(None, cx.executor())),
787 Default::default(),
788 &mut cx.to_async(),
789 )
790 .await
791 .unwrap();
792 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
793 .await;
794 tree.flush_fs_events(cx).await;
795 tree.read_with(cx, |tree, _| {
796 // Assert that file_scan_inclusions overrides file_scan_exclusions.
797 check_worktree_entries(
798 tree,
799 &[],
800 &["target", "node_modules"],
801 &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
802 &[
803 "node_modules/prettier/package.json",
804 ".DS_Store",
805 "node_modules/.DS_Store",
806 "src/.DS_Store",
807 ],
808 )
809 });
810}
811
812#[gpui::test]
813async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext) {
814 init_test(cx);
815 cx.executor().allow_parking();
816 let dir = TempTree::new(json!({
817 ".gitignore": "**/target\n/node_modules\n",
818 "target": {
819 "index": "blah2"
820 },
821 "node_modules": {
822 ".DS_Store": "",
823 "prettier": {
824 "package.json": "{}",
825 },
826 },
827 "src": {
828 ".DS_Store": "",
829 "foo": {
830 "foo.rs": "mod another;\n",
831 "another.rs": "// another",
832 },
833 },
834 ".DS_Store": "",
835 }));
836
837 cx.update(|cx| {
838 cx.update_global::<SettingsStore, _>(|store, cx| {
839 store.update_user_settings(cx, |settings| {
840 settings.project.worktree.file_scan_exclusions =
841 Some(vec!["**/.DS_Store".to_string()]);
842 settings.project.worktree.file_scan_inclusions =
843 Some(vec!["**/.DS_Store".to_string()]);
844 });
845 });
846 });
847
848 let tree = Worktree::local(
849 dir.path(),
850 true,
851 Arc::new(RealFs::new(None, cx.executor())),
852 Default::default(),
853 &mut cx.to_async(),
854 )
855 .await
856 .unwrap();
857 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
858 .await;
859 tree.flush_fs_events(cx).await;
860 tree.read_with(cx, |tree, _| {
861 // Assert that file_scan_inclusions overrides file_scan_exclusions.
862 check_worktree_entries(
863 tree,
864 &[".DS_Store, src/.DS_Store"],
865 &["target", "node_modules"],
866 &["src/foo/another.rs", "src/foo/foo.rs", ".gitignore"],
867 &[],
868 )
869 });
870}
871
872#[gpui::test]
873async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppContext) {
874 init_test(cx);
875 cx.executor().allow_parking();
876 let dir = TempTree::new(json!({
877 ".gitignore": "**/target\n/node_modules/\n",
878 "target": {
879 "index": "blah2"
880 },
881 "node_modules": {
882 ".DS_Store": "",
883 "prettier": {
884 "package.json": "{}",
885 },
886 },
887 "src": {
888 ".DS_Store": "",
889 "foo": {
890 "foo.rs": "mod another;\n",
891 "another.rs": "// another",
892 },
893 },
894 ".DS_Store": "",
895 }));
896
897 cx.update(|cx| {
898 cx.update_global::<SettingsStore, _>(|store, cx| {
899 store.update_user_settings(cx, |settings| {
900 settings.project.worktree.file_scan_exclusions = Some(vec![]);
901 settings.project.worktree.file_scan_inclusions =
902 Some(vec!["node_modules/**".to_string()]);
903 });
904 });
905 });
906 let tree = Worktree::local(
907 dir.path(),
908 true,
909 Arc::new(RealFs::new(None, cx.executor())),
910 Default::default(),
911 &mut cx.to_async(),
912 )
913 .await
914 .unwrap();
915 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
916 .await;
917 tree.flush_fs_events(cx).await;
918
919 tree.read_with(cx, |tree, _| {
920 assert!(
921 tree.entry_for_path("node_modules")
922 .is_some_and(|f| f.is_always_included)
923 );
924 assert!(
925 tree.entry_for_path("node_modules/prettier/package.json")
926 .is_some_and(|f| f.is_always_included)
927 );
928 });
929
930 cx.update(|cx| {
931 cx.update_global::<SettingsStore, _>(|store, cx| {
932 store.update_user_settings(cx, |settings| {
933 settings.project.worktree.file_scan_exclusions = Some(vec![]);
934 settings.project.worktree.file_scan_inclusions = Some(vec![]);
935 });
936 });
937 });
938 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
939 .await;
940 tree.flush_fs_events(cx).await;
941
942 tree.read_with(cx, |tree, _| {
943 assert!(
944 tree.entry_for_path("node_modules")
945 .is_some_and(|f| !f.is_always_included)
946 );
947 assert!(
948 tree.entry_for_path("node_modules/prettier/package.json")
949 .is_some_and(|f| !f.is_always_included)
950 );
951 });
952}
953
954#[gpui::test]
955async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
956 init_test(cx);
957 cx.executor().allow_parking();
958 let dir = TempTree::new(json!({
959 ".gitignore": "**/target\n/node_modules\n",
960 "target": {
961 "index": "blah2"
962 },
963 "node_modules": {
964 ".DS_Store": "",
965 "prettier": {
966 "package.json": "{}",
967 },
968 },
969 "src": {
970 ".DS_Store": "",
971 "foo": {
972 "foo.rs": "mod another;\n",
973 "another.rs": "// another",
974 },
975 "bar": {
976 "bar.rs": "// bar",
977 },
978 "lib.rs": "mod foo;\nmod bar;\n",
979 },
980 ".DS_Store": "",
981 }));
982 cx.update(|cx| {
983 cx.update_global::<SettingsStore, _>(|store, cx| {
984 store.update_user_settings(cx, |settings| {
985 settings.project.worktree.file_scan_exclusions =
986 Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
987 });
988 });
989 });
990
991 let tree = Worktree::local(
992 dir.path(),
993 true,
994 Arc::new(RealFs::new(None, cx.executor())),
995 Default::default(),
996 &mut cx.to_async(),
997 )
998 .await
999 .unwrap();
1000 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1001 .await;
1002 tree.flush_fs_events(cx).await;
1003 tree.read_with(cx, |tree, _| {
1004 check_worktree_entries(
1005 tree,
1006 &[
1007 "src/foo/foo.rs",
1008 "src/foo/another.rs",
1009 "node_modules/.DS_Store",
1010 "src/.DS_Store",
1011 ".DS_Store",
1012 ],
1013 &["target", "node_modules"],
1014 &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
1015 &[],
1016 )
1017 });
1018
1019 cx.update(|cx| {
1020 cx.update_global::<SettingsStore, _>(|store, cx| {
1021 store.update_user_settings(cx, |settings| {
1022 settings.project.worktree.file_scan_exclusions =
1023 Some(vec!["**/node_modules/**".to_string()]);
1024 });
1025 });
1026 });
1027 tree.flush_fs_events(cx).await;
1028 cx.executor().run_until_parked();
1029 tree.read_with(cx, |tree, _| {
1030 check_worktree_entries(
1031 tree,
1032 &[
1033 "node_modules/prettier/package.json",
1034 "node_modules/.DS_Store",
1035 "node_modules",
1036 ],
1037 &["target"],
1038 &[
1039 ".gitignore",
1040 "src/lib.rs",
1041 "src/bar/bar.rs",
1042 "src/foo/foo.rs",
1043 "src/foo/another.rs",
1044 "src/.DS_Store",
1045 ".DS_Store",
1046 ],
1047 &[],
1048 )
1049 });
1050}
1051
1052#[gpui::test]
1053async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
1054 init_test(cx);
1055 cx.executor().allow_parking();
1056 let dir = TempTree::new(json!({
1057 ".git": {
1058 "HEAD": "ref: refs/heads/main\n",
1059 "foo": "bar",
1060 },
1061 ".gitignore": "**/target\n/node_modules\ntest_output\n",
1062 "target": {
1063 "index": "blah2"
1064 },
1065 "node_modules": {
1066 ".DS_Store": "",
1067 "prettier": {
1068 "package.json": "{}",
1069 },
1070 },
1071 "src": {
1072 ".DS_Store": "",
1073 "foo": {
1074 "foo.rs": "mod another;\n",
1075 "another.rs": "// another",
1076 },
1077 "bar": {
1078 "bar.rs": "// bar",
1079 },
1080 "lib.rs": "mod foo;\nmod bar;\n",
1081 },
1082 ".DS_Store": "",
1083 }));
1084 cx.update(|cx| {
1085 cx.update_global::<SettingsStore, _>(|store, cx| {
1086 store.update_user_settings(cx, |settings| {
1087 settings.project.worktree.file_scan_exclusions = Some(vec![
1088 "**/.git".to_string(),
1089 "node_modules/".to_string(),
1090 "build_output".to_string(),
1091 ]);
1092 });
1093 });
1094 });
1095
1096 let tree = Worktree::local(
1097 dir.path(),
1098 true,
1099 Arc::new(RealFs::new(None, cx.executor())),
1100 Default::default(),
1101 &mut cx.to_async(),
1102 )
1103 .await
1104 .unwrap();
1105 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1106 .await;
1107 tree.flush_fs_events(cx).await;
1108 tree.read_with(cx, |tree, _| {
1109 check_worktree_entries(
1110 tree,
1111 &[
1112 ".git/HEAD",
1113 ".git/foo",
1114 "node_modules",
1115 "node_modules/.DS_Store",
1116 "node_modules/prettier",
1117 "node_modules/prettier/package.json",
1118 ],
1119 &["target"],
1120 &[
1121 ".DS_Store",
1122 "src/.DS_Store",
1123 "src/lib.rs",
1124 "src/foo/foo.rs",
1125 "src/foo/another.rs",
1126 "src/bar/bar.rs",
1127 ".gitignore",
1128 ],
1129 &[],
1130 )
1131 });
1132
1133 let new_excluded_dir = dir.path().join("build_output");
1134 let new_ignored_dir = dir.path().join("test_output");
1135 std::fs::create_dir_all(&new_excluded_dir)
1136 .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
1137 std::fs::create_dir_all(&new_ignored_dir)
1138 .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
1139 let node_modules_dir = dir.path().join("node_modules");
1140 let dot_git_dir = dir.path().join(".git");
1141 let src_dir = dir.path().join("src");
1142 for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
1143 assert!(
1144 existing_dir.is_dir(),
1145 "Expect {existing_dir:?} to be present in the FS already"
1146 );
1147 }
1148
1149 for directory_for_new_file in [
1150 new_excluded_dir,
1151 new_ignored_dir,
1152 node_modules_dir,
1153 dot_git_dir,
1154 src_dir,
1155 ] {
1156 std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
1157 .unwrap_or_else(|e| {
1158 panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
1159 });
1160 }
1161 tree.flush_fs_events(cx).await;
1162
1163 tree.read_with(cx, |tree, _| {
1164 check_worktree_entries(
1165 tree,
1166 &[
1167 ".git/HEAD",
1168 ".git/foo",
1169 ".git/new_file",
1170 "node_modules",
1171 "node_modules/.DS_Store",
1172 "node_modules/prettier",
1173 "node_modules/prettier/package.json",
1174 "node_modules/new_file",
1175 "build_output",
1176 "build_output/new_file",
1177 "test_output/new_file",
1178 ],
1179 &["target", "test_output"],
1180 &[
1181 ".DS_Store",
1182 "src/.DS_Store",
1183 "src/lib.rs",
1184 "src/foo/foo.rs",
1185 "src/foo/another.rs",
1186 "src/bar/bar.rs",
1187 "src/new_file",
1188 ".gitignore",
1189 ],
1190 &[],
1191 )
1192 });
1193}
1194
1195#[gpui::test]
1196async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
1197 init_test(cx);
1198 cx.executor().allow_parking();
1199 let dir = TempTree::new(json!({
1200 ".git": {
1201 "HEAD": "ref: refs/heads/main\n",
1202 "foo": "foo contents",
1203 },
1204 }));
1205 let dot_git_worktree_dir = dir.path().join(".git");
1206
1207 let tree = Worktree::local(
1208 dot_git_worktree_dir.clone(),
1209 true,
1210 Arc::new(RealFs::new(None, cx.executor())),
1211 Default::default(),
1212 &mut cx.to_async(),
1213 )
1214 .await
1215 .unwrap();
1216 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1217 .await;
1218 tree.flush_fs_events(cx).await;
1219 tree.read_with(cx, |tree, _| {
1220 check_worktree_entries(tree, &[], &["HEAD", "foo"], &[], &[])
1221 });
1222
1223 std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
1224 .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
1225 tree.flush_fs_events(cx).await;
1226 tree.read_with(cx, |tree, _| {
1227 check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[], &[])
1228 });
1229}
1230
1231#[gpui::test(iterations = 30)]
1232async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
1233 init_test(cx);
1234 let fs = FakeFs::new(cx.background_executor.clone());
1235 fs.insert_tree(
1236 "/root",
1237 json!({
1238 "b": {},
1239 "c": {},
1240 "d": {},
1241 }),
1242 )
1243 .await;
1244
1245 let tree = Worktree::local(
1246 "/root".as_ref(),
1247 true,
1248 fs,
1249 Default::default(),
1250 &mut cx.to_async(),
1251 )
1252 .await
1253 .unwrap();
1254
1255 let snapshot1 = tree.update(cx, |tree, cx| {
1256 let tree = tree.as_local_mut().unwrap();
1257 let snapshot = Arc::new(Mutex::new(tree.snapshot()));
1258 tree.observe_updates(0, cx, {
1259 let snapshot = snapshot.clone();
1260 let settings = tree.settings();
1261 move |update| {
1262 snapshot
1263 .lock()
1264 .apply_remote_update(update, &settings.file_scan_inclusions);
1265 async { true }
1266 }
1267 });
1268 snapshot
1269 });
1270
1271 let entry = tree
1272 .update(cx, |tree, cx| {
1273 tree.as_local_mut()
1274 .unwrap()
1275 .create_entry("a/e".as_ref(), true, None, cx)
1276 })
1277 .await
1278 .unwrap()
1279 .into_included()
1280 .unwrap();
1281 assert!(entry.is_dir());
1282
1283 cx.executor().run_until_parked();
1284 tree.read_with(cx, |tree, _| {
1285 assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
1286 });
1287
1288 let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1289 assert_eq!(
1290 snapshot1.lock().entries(true, 0).collect::<Vec<_>>(),
1291 snapshot2.entries(true, 0).collect::<Vec<_>>()
1292 );
1293}
1294
1295#[gpui::test]
1296async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1297 init_test(cx);
1298 cx.executor().allow_parking();
1299
1300 let fs_fake = FakeFs::new(cx.background_executor.clone());
1301 fs_fake
1302 .insert_tree(
1303 "/root",
1304 json!({
1305 "a": {},
1306 }),
1307 )
1308 .await;
1309
1310 let tree_fake = Worktree::local(
1311 "/root".as_ref(),
1312 true,
1313 fs_fake,
1314 Default::default(),
1315 &mut cx.to_async(),
1316 )
1317 .await
1318 .unwrap();
1319
1320 let entry = tree_fake
1321 .update(cx, |tree, cx| {
1322 tree.as_local_mut()
1323 .unwrap()
1324 .create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
1325 })
1326 .await
1327 .unwrap()
1328 .into_included()
1329 .unwrap();
1330 assert!(entry.is_file());
1331
1332 cx.executor().run_until_parked();
1333 tree_fake.read_with(cx, |tree, _| {
1334 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1335 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1336 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1337 });
1338
1339 let fs_real = Arc::new(RealFs::new(None, cx.executor()));
1340 let temp_root = TempTree::new(json!({
1341 "a": {}
1342 }));
1343
1344 let tree_real = Worktree::local(
1345 temp_root.path(),
1346 true,
1347 fs_real,
1348 Default::default(),
1349 &mut cx.to_async(),
1350 )
1351 .await
1352 .unwrap();
1353
1354 let entry = tree_real
1355 .update(cx, |tree, cx| {
1356 tree.as_local_mut()
1357 .unwrap()
1358 .create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
1359 })
1360 .await
1361 .unwrap()
1362 .into_included()
1363 .unwrap();
1364 assert!(entry.is_file());
1365
1366 cx.executor().run_until_parked();
1367 tree_real.read_with(cx, |tree, _| {
1368 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1369 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1370 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1371 });
1372
1373 // Test smallest change
1374 let entry = tree_real
1375 .update(cx, |tree, cx| {
1376 tree.as_local_mut()
1377 .unwrap()
1378 .create_entry("a/b/c/e.txt".as_ref(), false, None, cx)
1379 })
1380 .await
1381 .unwrap()
1382 .into_included()
1383 .unwrap();
1384 assert!(entry.is_file());
1385
1386 cx.executor().run_until_parked();
1387 tree_real.read_with(cx, |tree, _| {
1388 assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
1389 });
1390
1391 // Test largest change
1392 let entry = tree_real
1393 .update(cx, |tree, cx| {
1394 tree.as_local_mut()
1395 .unwrap()
1396 .create_entry("d/e/f/g.txt".as_ref(), false, None, cx)
1397 })
1398 .await
1399 .unwrap()
1400 .into_included()
1401 .unwrap();
1402 assert!(entry.is_file());
1403
1404 cx.executor().run_until_parked();
1405 tree_real.read_with(cx, |tree, _| {
1406 assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
1407 assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
1408 assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
1409 assert!(tree.entry_for_path("d/").unwrap().is_dir());
1410 });
1411}
1412
1413#[gpui::test(iterations = 100)]
1414async fn test_random_worktree_operations_during_initial_scan(
1415 cx: &mut TestAppContext,
1416 mut rng: StdRng,
1417) {
1418 init_test(cx);
1419 let operations = env::var("OPERATIONS")
1420 .map(|o| o.parse().unwrap())
1421 .unwrap_or(5);
1422 let initial_entries = env::var("INITIAL_ENTRIES")
1423 .map(|o| o.parse().unwrap())
1424 .unwrap_or(20);
1425
1426 let root_dir = Path::new(path!("/test"));
1427 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1428 fs.as_fake().insert_tree(root_dir, json!({})).await;
1429 for _ in 0..initial_entries {
1430 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1431 }
1432 log::info!("generated initial tree");
1433
1434 let worktree = Worktree::local(
1435 root_dir,
1436 true,
1437 fs.clone(),
1438 Default::default(),
1439 &mut cx.to_async(),
1440 )
1441 .await
1442 .unwrap();
1443
1444 let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1445 let updates = Arc::new(Mutex::new(Vec::new()));
1446 worktree.update(cx, |tree, cx| {
1447 check_worktree_change_events(tree, cx);
1448
1449 tree.as_local_mut().unwrap().observe_updates(0, cx, {
1450 let updates = updates.clone();
1451 move |update| {
1452 updates.lock().push(update);
1453 async { true }
1454 }
1455 });
1456 });
1457
1458 for _ in 0..operations {
1459 worktree
1460 .update(cx, |worktree, cx| {
1461 randomly_mutate_worktree(worktree, &mut rng, cx)
1462 })
1463 .await
1464 .log_err();
1465 worktree.read_with(cx, |tree, _| {
1466 tree.as_local().unwrap().snapshot().check_invariants(true)
1467 });
1468
1469 if rng.random_bool(0.6) {
1470 snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1471 }
1472 }
1473
1474 worktree
1475 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1476 .await;
1477
1478 cx.executor().run_until_parked();
1479
1480 let final_snapshot = worktree.read_with(cx, |tree, _| {
1481 let tree = tree.as_local().unwrap();
1482 let snapshot = tree.snapshot();
1483 snapshot.check_invariants(true);
1484 snapshot
1485 });
1486
1487 let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1488
1489 for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1490 let mut updated_snapshot = snapshot.clone();
1491 for update in updates.lock().iter() {
1492 if update.scan_id >= updated_snapshot.scan_id() as u64 {
1493 updated_snapshot
1494 .apply_remote_update(update.clone(), &settings.file_scan_inclusions);
1495 }
1496 }
1497
1498 assert_eq!(
1499 updated_snapshot.entries(true, 0).collect::<Vec<_>>(),
1500 final_snapshot.entries(true, 0).collect::<Vec<_>>(),
1501 "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
1502 );
1503 }
1504}
1505
1506#[gpui::test(iterations = 100)]
1507async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
1508 init_test(cx);
1509 let operations = env::var("OPERATIONS")
1510 .map(|o| o.parse().unwrap())
1511 .unwrap_or(40);
1512 let initial_entries = env::var("INITIAL_ENTRIES")
1513 .map(|o| o.parse().unwrap())
1514 .unwrap_or(20);
1515
1516 let root_dir = Path::new(path!("/test"));
1517 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1518 fs.as_fake().insert_tree(root_dir, json!({})).await;
1519 for _ in 0..initial_entries {
1520 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1521 }
1522 log::info!("generated initial tree");
1523
1524 let worktree = Worktree::local(
1525 root_dir,
1526 true,
1527 fs.clone(),
1528 Default::default(),
1529 &mut cx.to_async(),
1530 )
1531 .await
1532 .unwrap();
1533
1534 let updates = Arc::new(Mutex::new(Vec::new()));
1535 worktree.update(cx, |tree, cx| {
1536 check_worktree_change_events(tree, cx);
1537
1538 tree.as_local_mut().unwrap().observe_updates(0, cx, {
1539 let updates = updates.clone();
1540 move |update| {
1541 updates.lock().push(update);
1542 async { true }
1543 }
1544 });
1545 });
1546
1547 worktree
1548 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1549 .await;
1550
1551 fs.as_fake().pause_events();
1552 let mut snapshots = Vec::new();
1553 let mut mutations_len = operations;
1554 while mutations_len > 1 {
1555 if rng.random_bool(0.2) {
1556 worktree
1557 .update(cx, |worktree, cx| {
1558 randomly_mutate_worktree(worktree, &mut rng, cx)
1559 })
1560 .await
1561 .log_err();
1562 } else {
1563 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1564 }
1565
1566 let buffered_event_count = fs.as_fake().buffered_event_count();
1567 if buffered_event_count > 0 && rng.random_bool(0.3) {
1568 let len = rng.random_range(0..=buffered_event_count);
1569 log::info!("flushing {} events", len);
1570 fs.as_fake().flush_events(len);
1571 } else {
1572 randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
1573 mutations_len -= 1;
1574 }
1575
1576 cx.executor().run_until_parked();
1577 if rng.random_bool(0.2) {
1578 log::info!("storing snapshot {}", snapshots.len());
1579 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1580 snapshots.push(snapshot);
1581 }
1582 }
1583
1584 log::info!("quiescing");
1585 fs.as_fake().flush_events(usize::MAX);
1586 cx.executor().run_until_parked();
1587
1588 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1589 snapshot.check_invariants(true);
1590 let expanded_paths = snapshot
1591 .expanded_entries()
1592 .map(|e| e.path.clone())
1593 .collect::<Vec<_>>();
1594
1595 {
1596 let new_worktree = Worktree::local(
1597 root_dir,
1598 true,
1599 fs.clone(),
1600 Default::default(),
1601 &mut cx.to_async(),
1602 )
1603 .await
1604 .unwrap();
1605 new_worktree
1606 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1607 .await;
1608 new_worktree
1609 .update(cx, |tree, _| {
1610 tree.as_local_mut()
1611 .unwrap()
1612 .refresh_entries_for_paths(expanded_paths)
1613 })
1614 .recv()
1615 .await;
1616 let new_snapshot =
1617 new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1618 assert_eq!(
1619 snapshot.entries_without_ids(true),
1620 new_snapshot.entries_without_ids(true)
1621 );
1622 }
1623
1624 let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1625
1626 for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
1627 for update in updates.lock().iter() {
1628 if update.scan_id >= prev_snapshot.scan_id() as u64 {
1629 prev_snapshot.apply_remote_update(update.clone(), &settings.file_scan_inclusions);
1630 }
1631 }
1632
1633 assert_eq!(
1634 prev_snapshot
1635 .entries(true, 0)
1636 .map(ignore_pending_dir)
1637 .collect::<Vec<_>>(),
1638 snapshot
1639 .entries(true, 0)
1640 .map(ignore_pending_dir)
1641 .collect::<Vec<_>>(),
1642 "wrong updates after snapshot {i}: {updates:#?}",
1643 );
1644 }
1645
1646 fn ignore_pending_dir(entry: &Entry) -> Entry {
1647 let mut entry = entry.clone();
1648 if entry.kind.is_dir() {
1649 entry.kind = EntryKind::Dir
1650 }
1651 entry
1652 }
1653}
1654
1655// The worktree's `UpdatedEntries` event can be used to follow along with
1656// all changes to the worktree's snapshot.
1657fn check_worktree_change_events(tree: &mut Worktree, cx: &mut Context<Worktree>) {
1658 let mut entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1659 cx.subscribe(&cx.entity(), move |tree, _, event, _| {
1660 if let Event::UpdatedEntries(changes) = event {
1661 for (path, _, change_type) in changes.iter() {
1662 let entry = tree.entry_for_path(path).cloned();
1663 let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
1664 Ok(ix) | Err(ix) => ix,
1665 };
1666 match change_type {
1667 PathChange::Added => entries.insert(ix, entry.unwrap()),
1668 PathChange::Removed => drop(entries.remove(ix)),
1669 PathChange::Updated => {
1670 let entry = entry.unwrap();
1671 let existing_entry = entries.get_mut(ix).unwrap();
1672 assert_eq!(existing_entry.path, entry.path);
1673 *existing_entry = entry;
1674 }
1675 PathChange::AddedOrUpdated | PathChange::Loaded => {
1676 let entry = entry.unwrap();
1677 if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
1678 *entries.get_mut(ix).unwrap() = entry;
1679 } else {
1680 entries.insert(ix, entry);
1681 }
1682 }
1683 }
1684 }
1685
1686 let new_entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1687 assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
1688 }
1689 })
1690 .detach();
1691}
1692
1693fn randomly_mutate_worktree(
1694 worktree: &mut Worktree,
1695 rng: &mut impl Rng,
1696 cx: &mut Context<Worktree>,
1697) -> Task<Result<()>> {
1698 log::info!("mutating worktree");
1699 let worktree = worktree.as_local_mut().unwrap();
1700 let snapshot = worktree.snapshot();
1701 let entry = snapshot.entries(false, 0).choose(rng).unwrap();
1702
1703 match rng.random_range(0_u32..100) {
1704 0..=33 if entry.path.as_ref() != Path::new("") => {
1705 log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
1706 worktree.delete_entry(entry.id, false, cx).unwrap()
1707 }
1708 ..=66 if entry.path.as_ref() != Path::new("") => {
1709 let other_entry = snapshot.entries(false, 0).choose(rng).unwrap();
1710 let new_parent_path = if other_entry.is_dir() {
1711 other_entry.path.clone()
1712 } else {
1713 other_entry.path.parent().unwrap().into()
1714 };
1715 let mut new_path = new_parent_path.join(random_filename(rng));
1716 if new_path.starts_with(&entry.path) {
1717 new_path = random_filename(rng).into();
1718 }
1719
1720 log::info!(
1721 "renaming entry {:?} ({}) to {:?}",
1722 entry.path,
1723 entry.id.0,
1724 new_path
1725 );
1726 let task = worktree.rename_entry(entry.id, new_path, cx);
1727 cx.background_spawn(async move {
1728 task.await?.into_included().unwrap();
1729 Ok(())
1730 })
1731 }
1732 _ => {
1733 if entry.is_dir() {
1734 let child_path = entry.path.join(random_filename(rng));
1735 let is_dir = rng.random_bool(0.3);
1736 log::info!(
1737 "creating {} at {:?}",
1738 if is_dir { "dir" } else { "file" },
1739 child_path,
1740 );
1741 let task = worktree.create_entry(child_path, is_dir, None, cx);
1742 cx.background_spawn(async move {
1743 task.await?;
1744 Ok(())
1745 })
1746 } else {
1747 log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
1748 let task =
1749 worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
1750 cx.background_spawn(async move {
1751 task.await?;
1752 Ok(())
1753 })
1754 }
1755 }
1756 }
1757}
1758
1759async fn randomly_mutate_fs(
1760 fs: &Arc<dyn Fs>,
1761 root_path: &Path,
1762 insertion_probability: f64,
1763 rng: &mut impl Rng,
1764) {
1765 log::info!("mutating fs");
1766 let mut files = Vec::new();
1767 let mut dirs = Vec::new();
1768 for path in fs.as_fake().paths(false) {
1769 if path.starts_with(root_path) {
1770 if fs.is_file(&path).await {
1771 files.push(path);
1772 } else {
1773 dirs.push(path);
1774 }
1775 }
1776 }
1777
1778 if (files.is_empty() && dirs.len() == 1) || rng.random_bool(insertion_probability) {
1779 let path = dirs.choose(rng).unwrap();
1780 let new_path = path.join(random_filename(rng));
1781
1782 if rng.random() {
1783 log::info!(
1784 "creating dir {:?}",
1785 new_path.strip_prefix(root_path).unwrap()
1786 );
1787 fs.create_dir(&new_path).await.unwrap();
1788 } else {
1789 log::info!(
1790 "creating file {:?}",
1791 new_path.strip_prefix(root_path).unwrap()
1792 );
1793 fs.create_file(&new_path, Default::default()).await.unwrap();
1794 }
1795 } else if rng.random_bool(0.05) {
1796 let ignore_dir_path = dirs.choose(rng).unwrap();
1797 let ignore_path = ignore_dir_path.join(*GITIGNORE);
1798
1799 let subdirs = dirs
1800 .iter()
1801 .filter(|d| d.starts_with(ignore_dir_path))
1802 .cloned()
1803 .collect::<Vec<_>>();
1804 let subfiles = files
1805 .iter()
1806 .filter(|d| d.starts_with(ignore_dir_path))
1807 .cloned()
1808 .collect::<Vec<_>>();
1809 let files_to_ignore = {
1810 let len = rng.random_range(0..=subfiles.len());
1811 subfiles.choose_multiple(rng, len)
1812 };
1813 let dirs_to_ignore = {
1814 let len = rng.random_range(0..subdirs.len());
1815 subdirs.choose_multiple(rng, len)
1816 };
1817
1818 let mut ignore_contents = String::new();
1819 for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
1820 writeln!(
1821 ignore_contents,
1822 "{}",
1823 path_to_ignore
1824 .strip_prefix(ignore_dir_path)
1825 .unwrap()
1826 .to_str()
1827 .unwrap()
1828 )
1829 .unwrap();
1830 }
1831 log::info!(
1832 "creating gitignore {:?} with contents:\n{}",
1833 ignore_path.strip_prefix(root_path).unwrap(),
1834 ignore_contents
1835 );
1836 fs.save(
1837 &ignore_path,
1838 &ignore_contents.as_str().into(),
1839 Default::default(),
1840 )
1841 .await
1842 .unwrap();
1843 } else {
1844 let old_path = {
1845 let file_path = files.choose(rng);
1846 let dir_path = dirs[1..].choose(rng);
1847 file_path.into_iter().chain(dir_path).choose(rng).unwrap()
1848 };
1849
1850 let is_rename = rng.random();
1851 if is_rename {
1852 let new_path_parent = dirs
1853 .iter()
1854 .filter(|d| !d.starts_with(old_path))
1855 .choose(rng)
1856 .unwrap();
1857
1858 let overwrite_existing_dir =
1859 !old_path.starts_with(new_path_parent) && rng.random_bool(0.3);
1860 let new_path = if overwrite_existing_dir {
1861 fs.remove_dir(
1862 new_path_parent,
1863 RemoveOptions {
1864 recursive: true,
1865 ignore_if_not_exists: true,
1866 },
1867 )
1868 .await
1869 .unwrap();
1870 new_path_parent.to_path_buf()
1871 } else {
1872 new_path_parent.join(random_filename(rng))
1873 };
1874
1875 log::info!(
1876 "renaming {:?} to {}{:?}",
1877 old_path.strip_prefix(root_path).unwrap(),
1878 if overwrite_existing_dir {
1879 "overwrite "
1880 } else {
1881 ""
1882 },
1883 new_path.strip_prefix(root_path).unwrap()
1884 );
1885 fs.rename(
1886 old_path,
1887 &new_path,
1888 fs::RenameOptions {
1889 overwrite: true,
1890 ignore_if_exists: true,
1891 },
1892 )
1893 .await
1894 .unwrap();
1895 } else if fs.is_file(old_path).await {
1896 log::info!(
1897 "deleting file {:?}",
1898 old_path.strip_prefix(root_path).unwrap()
1899 );
1900 fs.remove_file(old_path, Default::default()).await.unwrap();
1901 } else {
1902 log::info!(
1903 "deleting dir {:?}",
1904 old_path.strip_prefix(root_path).unwrap()
1905 );
1906 fs.remove_dir(
1907 old_path,
1908 RemoveOptions {
1909 recursive: true,
1910 ignore_if_not_exists: true,
1911 },
1912 )
1913 .await
1914 .unwrap();
1915 }
1916 }
1917}
1918
1919fn random_filename(rng: &mut impl Rng) -> String {
1920 (0..6)
1921 .map(|_| rng.sample(rand::distr::Alphanumeric))
1922 .map(char::from)
1923 .collect()
1924}
1925
1926#[gpui::test]
1927async fn test_rename_file_to_new_directory(cx: &mut TestAppContext) {
1928 init_test(cx);
1929 let fs = FakeFs::new(cx.background_executor.clone());
1930 let expected_contents = "content";
1931 fs.as_fake()
1932 .insert_tree(
1933 "/root",
1934 json!({
1935 "test.txt": expected_contents
1936 }),
1937 )
1938 .await;
1939 let worktree = Worktree::local(
1940 Path::new("/root"),
1941 true,
1942 fs.clone(),
1943 Arc::default(),
1944 &mut cx.to_async(),
1945 )
1946 .await
1947 .unwrap();
1948 cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
1949 .await;
1950
1951 let entry_id = worktree.read_with(cx, |worktree, _| {
1952 worktree.entry_for_path("test.txt").unwrap().id
1953 });
1954 let _result = worktree
1955 .update(cx, |worktree, cx| {
1956 worktree.rename_entry(entry_id, Path::new("dir1/dir2/dir3/test.txt"), cx)
1957 })
1958 .await
1959 .unwrap();
1960 worktree.read_with(cx, |worktree, _| {
1961 assert!(
1962 worktree.entry_for_path("test.txt").is_none(),
1963 "Old file should have been removed"
1964 );
1965 assert!(
1966 worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_some(),
1967 "Whole directory hierarchy and the new file should have been created"
1968 );
1969 });
1970 assert_eq!(
1971 worktree
1972 .update(cx, |worktree, cx| {
1973 worktree.load_file("dir1/dir2/dir3/test.txt".as_ref(), cx)
1974 })
1975 .await
1976 .unwrap()
1977 .text,
1978 expected_contents,
1979 "Moved file's contents should be preserved"
1980 );
1981
1982 let entry_id = worktree.read_with(cx, |worktree, _| {
1983 worktree
1984 .entry_for_path("dir1/dir2/dir3/test.txt")
1985 .unwrap()
1986 .id
1987 });
1988 let _result = worktree
1989 .update(cx, |worktree, cx| {
1990 worktree.rename_entry(entry_id, Path::new("dir1/dir2/test.txt"), cx)
1991 })
1992 .await
1993 .unwrap();
1994 worktree.read_with(cx, |worktree, _| {
1995 assert!(
1996 worktree.entry_for_path("test.txt").is_none(),
1997 "First file should not reappear"
1998 );
1999 assert!(
2000 worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_none(),
2001 "Old file should have been removed"
2002 );
2003 assert!(
2004 worktree.entry_for_path("dir1/dir2/test.txt").is_some(),
2005 "No error should have occurred after moving into existing directory"
2006 );
2007 });
2008 assert_eq!(
2009 worktree
2010 .update(cx, |worktree, cx| {
2011 worktree.load_file("dir1/dir2/test.txt".as_ref(), cx)
2012 })
2013 .await
2014 .unwrap()
2015 .text,
2016 expected_contents,
2017 "Moved file's contents should be preserved"
2018 );
2019}
2020
2021#[gpui::test]
2022async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
2023 init_test(cx);
2024 let fs = FakeFs::new(cx.background_executor.clone());
2025 fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
2026 .await;
2027 let tree = Worktree::local(
2028 Path::new("/.env"),
2029 true,
2030 fs.clone(),
2031 Default::default(),
2032 &mut cx.to_async(),
2033 )
2034 .await
2035 .unwrap();
2036 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2037 .await;
2038 tree.read_with(cx, |tree, _| {
2039 let entry = tree.entry_for_path("").unwrap();
2040 assert!(entry.is_private);
2041 });
2042}
2043
2044#[gpui::test]
2045fn test_unrelativize() {
2046 let work_directory = WorkDirectory::in_project("");
2047 pretty_assertions::assert_eq!(
2048 work_directory.try_unrelativize(&"crates/gpui/gpui.rs".into()),
2049 Some(Path::new("crates/gpui/gpui.rs").into())
2050 );
2051
2052 let work_directory = WorkDirectory::in_project("vendor/some-submodule");
2053 pretty_assertions::assert_eq!(
2054 work_directory.try_unrelativize(&"src/thing.c".into()),
2055 Some(Path::new("vendor/some-submodule/src/thing.c").into())
2056 );
2057
2058 let work_directory = WorkDirectory::AboveProject {
2059 absolute_path: Path::new("/projects/zed").into(),
2060 location_in_repo: Path::new("crates/gpui").into(),
2061 };
2062
2063 pretty_assertions::assert_eq!(
2064 work_directory.try_unrelativize(&"crates/util/util.rs".into()),
2065 None,
2066 );
2067
2068 pretty_assertions::assert_eq!(
2069 work_directory.unrelativize(&"crates/util/util.rs".into()),
2070 Path::new("../util/util.rs").into()
2071 );
2072
2073 pretty_assertions::assert_eq!(work_directory.try_unrelativize(&"README.md".into()), None,);
2074
2075 pretty_assertions::assert_eq!(
2076 work_directory.unrelativize(&"README.md".into()),
2077 Path::new("../../README.md").into()
2078 );
2079}
2080
2081#[gpui::test]
2082async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2083 init_test(cx);
2084
2085 let fs = FakeFs::new(executor);
2086 fs.insert_tree(
2087 path!("/root"),
2088 json!({
2089 ".git": {},
2090 "subproject": {
2091 "a.txt": "A"
2092 }
2093 }),
2094 )
2095 .await;
2096 let worktree = Worktree::local(
2097 path!("/root/subproject").as_ref(),
2098 true,
2099 fs.clone(),
2100 Arc::default(),
2101 &mut cx.to_async(),
2102 )
2103 .await
2104 .unwrap();
2105 worktree
2106 .update(cx, |worktree, _| {
2107 worktree.as_local().unwrap().scan_complete()
2108 })
2109 .await;
2110 cx.run_until_parked();
2111 let repos = worktree.update(cx, |worktree, _| {
2112 worktree
2113 .as_local()
2114 .unwrap()
2115 .git_repositories
2116 .values()
2117 .map(|entry| entry.work_directory_abs_path.clone())
2118 .collect::<Vec<_>>()
2119 });
2120 pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2121
2122 fs.touch_path(path!("/root/subproject")).await;
2123 worktree
2124 .update(cx, |worktree, _| {
2125 worktree.as_local().unwrap().scan_complete()
2126 })
2127 .await;
2128 cx.run_until_parked();
2129
2130 let repos = worktree.update(cx, |worktree, _| {
2131 worktree
2132 .as_local()
2133 .unwrap()
2134 .git_repositories
2135 .values()
2136 .map(|entry| entry.work_directory_abs_path.clone())
2137 .collect::<Vec<_>>()
2138 });
2139 pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2140}
2141
2142#[gpui::test]
2143async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2144 init_test(cx);
2145
2146 let home = paths::home_dir();
2147 let fs = FakeFs::new(executor);
2148 fs.insert_tree(
2149 home,
2150 json!({
2151 ".config": {
2152 "git": {
2153 "ignore": "foo\n/bar\nbaz\n"
2154 }
2155 },
2156 "project": {
2157 ".git": {},
2158 ".gitignore": "!baz",
2159 "foo": "",
2160 "bar": "",
2161 "sub": {
2162 "bar": "",
2163 },
2164 "subrepo": {
2165 ".git": {},
2166 "bar": ""
2167 },
2168 "baz": ""
2169 }
2170 }),
2171 )
2172 .await;
2173 let worktree = Worktree::local(
2174 home.join("project"),
2175 true,
2176 fs.clone(),
2177 Arc::default(),
2178 &mut cx.to_async(),
2179 )
2180 .await
2181 .unwrap();
2182 worktree
2183 .update(cx, |worktree, _| {
2184 worktree.as_local().unwrap().scan_complete()
2185 })
2186 .await;
2187 cx.run_until_parked();
2188
2189 // .gitignore overrides excludesFile, and anchored paths in excludesFile are resolved
2190 // relative to the nearest containing repository
2191 worktree.update(cx, |worktree, _cx| {
2192 check_worktree_entries(
2193 worktree,
2194 &[],
2195 &["foo", "bar", "subrepo/bar"],
2196 &["sub/bar", "baz"],
2197 &[],
2198 );
2199 });
2200
2201 // Ignore statuses are updated when excludesFile changes
2202 fs.write(
2203 &home.join(".config").join("git").join("ignore"),
2204 "/bar\nbaz\n".as_bytes(),
2205 )
2206 .await
2207 .unwrap();
2208 worktree
2209 .update(cx, |worktree, _| {
2210 worktree.as_local().unwrap().scan_complete()
2211 })
2212 .await;
2213 cx.run_until_parked();
2214
2215 worktree.update(cx, |worktree, _cx| {
2216 check_worktree_entries(
2217 worktree,
2218 &[],
2219 &["bar", "subrepo/bar"],
2220 &["foo", "sub/bar", "baz"],
2221 &[],
2222 );
2223 });
2224
2225 // Statuses are updated when .git added/removed
2226 fs.remove_dir(
2227 &home.join("project").join("subrepo").join(".git"),
2228 RemoveOptions {
2229 recursive: true,
2230 ..Default::default()
2231 },
2232 )
2233 .await
2234 .unwrap();
2235 worktree
2236 .update(cx, |worktree, _| {
2237 worktree.as_local().unwrap().scan_complete()
2238 })
2239 .await;
2240 cx.run_until_parked();
2241
2242 worktree.update(cx, |worktree, _cx| {
2243 check_worktree_entries(
2244 worktree,
2245 &[],
2246 &["bar"],
2247 &["foo", "sub/bar", "baz", "subrepo/bar"],
2248 &[],
2249 );
2250 });
2251}
2252
2253#[track_caller]
2254fn check_worktree_entries(
2255 tree: &Worktree,
2256 expected_excluded_paths: &[&str],
2257 expected_ignored_paths: &[&str],
2258 expected_tracked_paths: &[&str],
2259 expected_included_paths: &[&str],
2260) {
2261 for path in expected_excluded_paths {
2262 let entry = tree.entry_for_path(path);
2263 assert!(
2264 entry.is_none(),
2265 "expected path '{path}' to be excluded, but got entry: {entry:?}",
2266 );
2267 }
2268 for path in expected_ignored_paths {
2269 let entry = tree
2270 .entry_for_path(path)
2271 .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
2272 assert!(
2273 entry.is_ignored,
2274 "expected path '{path}' to be ignored, but got entry: {entry:?}",
2275 );
2276 }
2277 for path in expected_tracked_paths {
2278 let entry = tree
2279 .entry_for_path(path)
2280 .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
2281 assert!(
2282 !entry.is_ignored || entry.is_always_included,
2283 "expected path '{path}' to be tracked, but got entry: {entry:?}",
2284 );
2285 }
2286 for path in expected_included_paths {
2287 let entry = tree
2288 .entry_for_path(path)
2289 .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
2290 assert!(
2291 entry.is_always_included,
2292 "expected path '{path}' to always be included, but got entry: {entry:?}",
2293 );
2294 }
2295}
2296
2297fn init_test(cx: &mut gpui::TestAppContext) {
2298 zlog::init_test();
2299
2300 cx.update(|cx| {
2301 let settings_store = SettingsStore::test(cx);
2302 cx.set_global(settings_store);
2303 WorktreeSettings::register(cx);
2304 });
2305}