1mod worktree_settings;
2
3use anyhow::Result;
4use encoding_rs;
5use fs::{FakeFs, Fs, RealFs, RemoveOptions};
6use git::{DOT_GIT, GITIGNORE, REPO_EXCLUDE};
7use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext};
8use parking_lot::Mutex;
9use postage::stream::Stream;
10use pretty_assertions::assert_eq;
11use rand::prelude::*;
12use worktree::{Entry, EntryKind, Event, PathChange, Worktree, WorktreeModelHandle};
13
14use serde_json::json;
15use settings::{SettingsStore, WorktreeId};
16use std::{
17 env,
18 fmt::Write,
19 mem,
20 path::{Path, PathBuf},
21 sync::Arc,
22};
23use util::{
24 ResultExt, path,
25 paths::PathStyle,
26 rel_path::{RelPath, rel_path},
27 test::TempTree,
28};
29
30#[gpui::test]
31async fn test_traversal(cx: &mut TestAppContext) {
32 init_test(cx);
33 let fs = FakeFs::new(cx.background_executor.clone());
34 fs.insert_tree(
35 "/root",
36 json!({
37 ".gitignore": "a/b\n",
38 "a": {
39 "b": "",
40 "c": "",
41 }
42 }),
43 )
44 .await;
45
46 let tree = Worktree::local(
47 Path::new("/root"),
48 true,
49 fs,
50 Default::default(),
51 true,
52 WorktreeId::from_proto(0),
53 &mut cx.to_async(),
54 )
55 .await
56 .unwrap();
57 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
58 .await;
59
60 tree.read_with(cx, |tree, _| {
61 assert_eq!(
62 tree.entries(false, 0)
63 .map(|entry| entry.path.as_ref())
64 .collect::<Vec<_>>(),
65 vec![
66 rel_path(""),
67 rel_path(".gitignore"),
68 rel_path("a"),
69 rel_path("a/c"),
70 ]
71 );
72 assert_eq!(
73 tree.entries(true, 0)
74 .map(|entry| entry.path.as_ref())
75 .collect::<Vec<_>>(),
76 vec![
77 rel_path(""),
78 rel_path(".gitignore"),
79 rel_path("a"),
80 rel_path("a/b"),
81 rel_path("a/c"),
82 ]
83 );
84 })
85}
86
87#[gpui::test(iterations = 10)]
88async fn test_circular_symlinks(cx: &mut TestAppContext) {
89 init_test(cx);
90 let fs = FakeFs::new(cx.background_executor.clone());
91 fs.insert_tree(
92 "/root",
93 json!({
94 "lib": {
95 "a": {
96 "a.txt": ""
97 },
98 "b": {
99 "b.txt": ""
100 }
101 }
102 }),
103 )
104 .await;
105 fs.create_symlink("/root/lib/a/lib".as_ref(), "..".into())
106 .await
107 .unwrap();
108 fs.create_symlink("/root/lib/b/lib".as_ref(), "..".into())
109 .await
110 .unwrap();
111
112 let tree = Worktree::local(
113 Path::new("/root"),
114 true,
115 fs.clone(),
116 Default::default(),
117 true,
118 WorktreeId::from_proto(0),
119 &mut cx.to_async(),
120 )
121 .await
122 .unwrap();
123
124 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
125 .await;
126
127 tree.read_with(cx, |tree, _| {
128 assert_eq!(
129 tree.entries(false, 0)
130 .map(|entry| entry.path.as_ref())
131 .collect::<Vec<_>>(),
132 vec![
133 rel_path(""),
134 rel_path("lib"),
135 rel_path("lib/a"),
136 rel_path("lib/a/a.txt"),
137 rel_path("lib/a/lib"),
138 rel_path("lib/b"),
139 rel_path("lib/b/b.txt"),
140 rel_path("lib/b/lib"),
141 ]
142 );
143 });
144
145 fs.rename(
146 Path::new("/root/lib/a/lib"),
147 Path::new("/root/lib/a/lib-2"),
148 Default::default(),
149 )
150 .await
151 .unwrap();
152 cx.executor().run_until_parked();
153 tree.read_with(cx, |tree, _| {
154 assert_eq!(
155 tree.entries(false, 0)
156 .map(|entry| entry.path.as_ref())
157 .collect::<Vec<_>>(),
158 vec![
159 rel_path(""),
160 rel_path("lib"),
161 rel_path("lib/a"),
162 rel_path("lib/a/a.txt"),
163 rel_path("lib/a/lib-2"),
164 rel_path("lib/b"),
165 rel_path("lib/b/b.txt"),
166 rel_path("lib/b/lib"),
167 ]
168 );
169 });
170}
171
172#[gpui::test]
173async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
174 init_test(cx);
175 let fs = FakeFs::new(cx.background_executor.clone());
176 fs.insert_tree(
177 "/root",
178 json!({
179 "dir1": {
180 "deps": {
181 // symlinks here
182 },
183 "src": {
184 "a.rs": "",
185 "b.rs": "",
186 },
187 },
188 "dir2": {
189 "src": {
190 "c.rs": "",
191 "d.rs": "",
192 }
193 },
194 "dir3": {
195 "deps": {},
196 "src": {
197 "e.rs": "",
198 "f.rs": "",
199 },
200 }
201 }),
202 )
203 .await;
204
205 // These symlinks point to directories outside of the worktree's root, dir1.
206 fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into())
207 .await
208 .unwrap();
209 fs.create_symlink("/root/dir1/deps/dep-dir3".as_ref(), "../../dir3".into())
210 .await
211 .unwrap();
212
213 let tree = Worktree::local(
214 Path::new("/root/dir1"),
215 true,
216 fs.clone(),
217 Default::default(),
218 true,
219 WorktreeId::from_proto(0),
220 &mut cx.to_async(),
221 )
222 .await
223 .unwrap();
224
225 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
226 .await;
227
228 let tree_updates = Arc::new(Mutex::new(Vec::new()));
229 tree.update(cx, |_, cx| {
230 let tree_updates = tree_updates.clone();
231 cx.subscribe(&tree, move |_, _, event, _| {
232 if let Event::UpdatedEntries(update) = event {
233 tree_updates.lock().extend(
234 update
235 .iter()
236 .map(|(path, _, change)| (path.clone(), *change)),
237 );
238 }
239 })
240 .detach();
241 });
242
243 // The symlinked directories are not scanned by default.
244 tree.read_with(cx, |tree, _| {
245 assert_eq!(
246 tree.entries(true, 0)
247 .map(|entry| (entry.path.as_ref(), entry.is_external))
248 .collect::<Vec<_>>(),
249 vec![
250 (rel_path(""), false),
251 (rel_path("deps"), false),
252 (rel_path("deps/dep-dir2"), true),
253 (rel_path("deps/dep-dir3"), true),
254 (rel_path("src"), false),
255 (rel_path("src/a.rs"), false),
256 (rel_path("src/b.rs"), false),
257 ]
258 );
259
260 assert_eq!(
261 tree.entry_for_path(rel_path("deps/dep-dir2")).unwrap().kind,
262 EntryKind::UnloadedDir
263 );
264 });
265
266 // Expand one of the symlinked directories.
267 tree.read_with(cx, |tree, _| {
268 tree.as_local()
269 .unwrap()
270 .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3").into()])
271 })
272 .recv()
273 .await;
274
275 // The expanded directory's contents are loaded. Subdirectories are
276 // not scanned yet.
277 tree.read_with(cx, |tree, _| {
278 assert_eq!(
279 tree.entries(true, 0)
280 .map(|entry| (entry.path.as_ref(), entry.is_external))
281 .collect::<Vec<_>>(),
282 vec![
283 (rel_path(""), false),
284 (rel_path("deps"), false),
285 (rel_path("deps/dep-dir2"), true),
286 (rel_path("deps/dep-dir3"), true),
287 (rel_path("deps/dep-dir3/deps"), true),
288 (rel_path("deps/dep-dir3/src"), true),
289 (rel_path("src"), false),
290 (rel_path("src/a.rs"), false),
291 (rel_path("src/b.rs"), false),
292 ]
293 );
294 });
295 assert_eq!(
296 mem::take(&mut *tree_updates.lock()),
297 &[
298 (rel_path("deps/dep-dir3").into(), PathChange::Loaded),
299 (rel_path("deps/dep-dir3/deps").into(), PathChange::Loaded),
300 (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded)
301 ]
302 );
303
304 // Expand a subdirectory of one of the symlinked directories.
305 tree.read_with(cx, |tree, _| {
306 tree.as_local()
307 .unwrap()
308 .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3/src").into()])
309 })
310 .recv()
311 .await;
312
313 // The expanded subdirectory's contents are loaded.
314 tree.read_with(cx, |tree, _| {
315 assert_eq!(
316 tree.entries(true, 0)
317 .map(|entry| (entry.path.as_ref(), entry.is_external))
318 .collect::<Vec<_>>(),
319 vec![
320 (rel_path(""), false),
321 (rel_path("deps"), false),
322 (rel_path("deps/dep-dir2"), true),
323 (rel_path("deps/dep-dir3"), true),
324 (rel_path("deps/dep-dir3/deps"), true),
325 (rel_path("deps/dep-dir3/src"), true),
326 (rel_path("deps/dep-dir3/src/e.rs"), true),
327 (rel_path("deps/dep-dir3/src/f.rs"), true),
328 (rel_path("src"), false),
329 (rel_path("src/a.rs"), false),
330 (rel_path("src/b.rs"), false),
331 ]
332 );
333 });
334
335 assert_eq!(
336 mem::take(&mut *tree_updates.lock()),
337 &[
338 (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded),
339 (
340 rel_path("deps/dep-dir3/src/e.rs").into(),
341 PathChange::Loaded
342 ),
343 (
344 rel_path("deps/dep-dir3/src/f.rs").into(),
345 PathChange::Loaded
346 )
347 ]
348 );
349}
350
351#[cfg(target_os = "macos")]
352#[gpui::test]
353async fn test_renaming_case_only(cx: &mut TestAppContext) {
354 cx.executor().allow_parking();
355 init_test(cx);
356
357 const OLD_NAME: &str = "aaa.rs";
358 const NEW_NAME: &str = "AAA.rs";
359
360 let fs = Arc::new(RealFs::new(None, cx.executor()));
361 let temp_root = TempTree::new(json!({
362 OLD_NAME: "",
363 }));
364
365 let tree = Worktree::local(
366 temp_root.path(),
367 true,
368 fs.clone(),
369 Default::default(),
370 true,
371 WorktreeId::from_proto(0),
372 &mut cx.to_async(),
373 )
374 .await
375 .unwrap();
376
377 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
378 .await;
379 tree.read_with(cx, |tree, _| {
380 assert_eq!(
381 tree.entries(true, 0)
382 .map(|entry| entry.path.as_ref())
383 .collect::<Vec<_>>(),
384 vec![rel_path(""), rel_path(OLD_NAME)]
385 );
386 });
387
388 fs.rename(
389 &temp_root.path().join(OLD_NAME),
390 &temp_root.path().join(NEW_NAME),
391 fs::RenameOptions {
392 overwrite: true,
393 ignore_if_exists: true,
394 create_parents: false,
395 },
396 )
397 .await
398 .unwrap();
399
400 tree.flush_fs_events(cx).await;
401
402 tree.read_with(cx, |tree, _| {
403 assert_eq!(
404 tree.entries(true, 0)
405 .map(|entry| entry.path.as_ref())
406 .collect::<Vec<_>>(),
407 vec![rel_path(""), rel_path(NEW_NAME)]
408 );
409 });
410}
411
412#[gpui::test]
413async fn test_root_rescan_reconciles_stale_state(cx: &mut TestAppContext) {
414 init_test(cx);
415 let fs = FakeFs::new(cx.background_executor.clone());
416 fs.insert_tree(
417 "/root",
418 json!({
419 "old.txt": "",
420 }),
421 )
422 .await;
423
424 let tree = Worktree::local(
425 Path::new("/root"),
426 true,
427 fs.clone(),
428 Default::default(),
429 true,
430 WorktreeId::from_proto(0),
431 &mut cx.to_async(),
432 )
433 .await
434 .unwrap();
435
436 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
437 .await;
438
439 tree.read_with(cx, |tree, _| {
440 assert_eq!(
441 tree.entries(true, 0)
442 .map(|entry| entry.path.as_ref())
443 .collect::<Vec<_>>(),
444 vec![rel_path(""), rel_path("old.txt")]
445 );
446 });
447
448 fs.pause_events();
449 fs.remove_file(Path::new("/root/old.txt"), RemoveOptions::default())
450 .await
451 .unwrap();
452 fs.insert_file(Path::new("/root/new.txt"), Vec::new()).await;
453 assert_eq!(fs.buffered_event_count(), 2);
454 fs.clear_buffered_events();
455
456 tree.read_with(cx, |tree, _| {
457 assert!(tree.entry_for_path(rel_path("old.txt")).is_some());
458 assert!(tree.entry_for_path(rel_path("new.txt")).is_none());
459 });
460
461 fs.emit_fs_event("/root", Some(fs::PathEventKind::Rescan));
462 fs.unpause_events_and_flush();
463 tree.flush_fs_events(cx).await;
464
465 tree.read_with(cx, |tree, _| {
466 assert!(tree.entry_for_path(rel_path("old.txt")).is_none());
467 assert!(tree.entry_for_path(rel_path("new.txt")).is_some());
468 assert_eq!(
469 tree.entries(true, 0)
470 .map(|entry| entry.path.as_ref())
471 .collect::<Vec<_>>(),
472 vec![rel_path(""), rel_path("new.txt")]
473 );
474 });
475}
476
477#[gpui::test]
478async fn test_subtree_rescan_reports_unchanged_descendants_as_updated(cx: &mut TestAppContext) {
479 init_test(cx);
480 let fs = FakeFs::new(cx.background_executor.clone());
481 fs.insert_tree(
482 "/root",
483 json!({
484 "dir": {
485 "child.txt": "",
486 "nested": {
487 "grandchild.txt": "",
488 },
489 "remove": {
490 "removed.txt": "",
491 }
492 },
493 "other.txt": "",
494 }),
495 )
496 .await;
497
498 let tree = Worktree::local(
499 Path::new("/root"),
500 true,
501 fs.clone(),
502 Default::default(),
503 true,
504 WorktreeId::from_proto(0),
505 &mut cx.to_async(),
506 )
507 .await
508 .unwrap();
509
510 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
511 .await;
512
513 let tree_updates = Arc::new(Mutex::new(Vec::new()));
514 tree.update(cx, |_, cx| {
515 let tree_updates = tree_updates.clone();
516 cx.subscribe(&tree, move |_, _, event, _| {
517 if let Event::UpdatedEntries(update) = event {
518 tree_updates.lock().extend(
519 update
520 .iter()
521 .filter(|(path, _, _)| path.as_ref() != rel_path("fs-event-sentinel"))
522 .map(|(path, _, change)| (path.clone(), *change)),
523 );
524 }
525 })
526 .detach();
527 });
528 fs.pause_events();
529 fs.insert_file("/root/dir/new.txt", b"new content".to_vec())
530 .await;
531 fs.remove_dir(
532 "/root/dir/remove".as_ref(),
533 RemoveOptions {
534 recursive: true,
535 ignore_if_not_exists: false,
536 },
537 )
538 .await
539 .unwrap();
540 fs.clear_buffered_events();
541 fs.unpause_events_and_flush();
542
543 fs.emit_fs_event("/root/dir", Some(fs::PathEventKind::Rescan));
544 tree.flush_fs_events(cx).await;
545
546 assert_eq!(
547 mem::take(&mut *tree_updates.lock()),
548 &[
549 (rel_path("dir").into(), PathChange::Updated),
550 (rel_path("dir/child.txt").into(), PathChange::Updated),
551 (rel_path("dir/nested").into(), PathChange::Updated),
552 (
553 rel_path("dir/nested/grandchild.txt").into(),
554 PathChange::Updated
555 ),
556 (rel_path("dir/new.txt").into(), PathChange::Added),
557 (rel_path("dir/remove").into(), PathChange::Removed),
558 (
559 rel_path("dir/remove/removed.txt").into(),
560 PathChange::Removed
561 ),
562 ]
563 );
564
565 tree.read_with(cx, |tree, _| {
566 assert!(tree.entry_for_path(rel_path("other.txt")).is_some());
567 });
568}
569
570#[gpui::test]
571async fn test_open_gitignored_files(cx: &mut TestAppContext) {
572 init_test(cx);
573 let fs = FakeFs::new(cx.background_executor.clone());
574 fs.insert_tree(
575 "/root",
576 json!({
577 ".gitignore": "node_modules\n",
578 "one": {
579 "node_modules": {
580 "a": {
581 "a1.js": "a1",
582 "a2.js": "a2",
583 },
584 "b": {
585 "b1.js": "b1",
586 "b2.js": "b2",
587 },
588 "c": {
589 "c1.js": "c1",
590 "c2.js": "c2",
591 }
592 },
593 },
594 "two": {
595 "x.js": "",
596 "y.js": "",
597 },
598 }),
599 )
600 .await;
601
602 let tree = Worktree::local(
603 Path::new("/root"),
604 true,
605 fs.clone(),
606 Default::default(),
607 true,
608 WorktreeId::from_proto(0),
609 &mut cx.to_async(),
610 )
611 .await
612 .unwrap();
613
614 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
615 .await;
616
617 tree.read_with(cx, |tree, _| {
618 assert_eq!(
619 tree.entries(true, 0)
620 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
621 .collect::<Vec<_>>(),
622 vec![
623 (rel_path(""), false),
624 (rel_path(".gitignore"), false),
625 (rel_path("one"), false),
626 (rel_path("one/node_modules"), true),
627 (rel_path("two"), false),
628 (rel_path("two/x.js"), false),
629 (rel_path("two/y.js"), false),
630 ]
631 );
632 });
633
634 // Open a file that is nested inside of a gitignored directory that
635 // has not yet been expanded.
636 let prev_read_dir_count = fs.read_dir_call_count();
637 let loaded = tree
638 .update(cx, |tree, cx| {
639 tree.load_file(rel_path("one/node_modules/b/b1.js"), cx)
640 })
641 .await
642 .unwrap();
643
644 tree.read_with(cx, |tree, _| {
645 assert_eq!(
646 tree.entries(true, 0)
647 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
648 .collect::<Vec<_>>(),
649 vec![
650 (rel_path(""), false),
651 (rel_path(".gitignore"), false),
652 (rel_path("one"), false),
653 (rel_path("one/node_modules"), true),
654 (rel_path("one/node_modules/a"), true),
655 (rel_path("one/node_modules/b"), true),
656 (rel_path("one/node_modules/b/b1.js"), true),
657 (rel_path("one/node_modules/b/b2.js"), true),
658 (rel_path("one/node_modules/c"), true),
659 (rel_path("two"), false),
660 (rel_path("two/x.js"), false),
661 (rel_path("two/y.js"), false),
662 ]
663 );
664
665 assert_eq!(
666 loaded.file.path.as_ref(),
667 rel_path("one/node_modules/b/b1.js")
668 );
669
670 // Only the newly-expanded directories are scanned.
671 assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2);
672 });
673
674 // Open another file in a different subdirectory of the same
675 // gitignored directory.
676 let prev_read_dir_count = fs.read_dir_call_count();
677 let loaded = tree
678 .update(cx, |tree, cx| {
679 tree.load_file(rel_path("one/node_modules/a/a2.js"), cx)
680 })
681 .await
682 .unwrap();
683
684 tree.read_with(cx, |tree, _| {
685 assert_eq!(
686 tree.entries(true, 0)
687 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
688 .collect::<Vec<_>>(),
689 vec![
690 (rel_path(""), false),
691 (rel_path(".gitignore"), false),
692 (rel_path("one"), false),
693 (rel_path("one/node_modules"), true),
694 (rel_path("one/node_modules/a"), true),
695 (rel_path("one/node_modules/a/a1.js"), true),
696 (rel_path("one/node_modules/a/a2.js"), true),
697 (rel_path("one/node_modules/b"), true),
698 (rel_path("one/node_modules/b/b1.js"), true),
699 (rel_path("one/node_modules/b/b2.js"), true),
700 (rel_path("one/node_modules/c"), true),
701 (rel_path("two"), false),
702 (rel_path("two/x.js"), false),
703 (rel_path("two/y.js"), false),
704 ]
705 );
706
707 assert_eq!(
708 loaded.file.path.as_ref(),
709 rel_path("one/node_modules/a/a2.js")
710 );
711
712 // Only the newly-expanded directory is scanned.
713 assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
714 });
715
716 let path = PathBuf::from("/root/one/node_modules/c/lib");
717
718 // No work happens when files and directories change within an unloaded directory.
719 let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count();
720 // When we open a directory, we check each ancestor whether it's a git
721 // repository. That means we have an fs.metadata call per ancestor that we
722 // need to subtract here.
723 let ancestors = path.ancestors().count();
724
725 fs.create_dir(path.as_ref()).await.unwrap();
726 cx.executor().run_until_parked();
727
728 assert_eq!(
729 fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count - ancestors,
730 0
731 );
732}
733
734#[gpui::test]
735async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
736 init_test(cx);
737 let fs = FakeFs::new(cx.background_executor.clone());
738 fs.insert_tree(
739 "/root",
740 json!({
741 ".gitignore": "node_modules\n",
742 "a": {
743 "a.js": "",
744 },
745 "b": {
746 "b.js": "",
747 },
748 "node_modules": {
749 "c": {
750 "c.js": "",
751 },
752 "d": {
753 "d.js": "",
754 "e": {
755 "e1.js": "",
756 "e2.js": "",
757 },
758 "f": {
759 "f1.js": "",
760 "f2.js": "",
761 }
762 },
763 },
764 }),
765 )
766 .await;
767
768 let tree = Worktree::local(
769 Path::new("/root"),
770 true,
771 fs.clone(),
772 Default::default(),
773 true,
774 WorktreeId::from_proto(0),
775 &mut cx.to_async(),
776 )
777 .await
778 .unwrap();
779
780 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
781 .await;
782
783 // Open a file within the gitignored directory, forcing some of its
784 // subdirectories to be read, but not all.
785 let read_dir_count_1 = fs.read_dir_call_count();
786 tree.read_with(cx, |tree, _| {
787 tree.as_local()
788 .unwrap()
789 .refresh_entries_for_paths(vec![rel_path("node_modules/d/d.js").into()])
790 })
791 .recv()
792 .await;
793
794 // Those subdirectories are now loaded.
795 tree.read_with(cx, |tree, _| {
796 assert_eq!(
797 tree.entries(true, 0)
798 .map(|e| (e.path.as_ref(), e.is_ignored))
799 .collect::<Vec<_>>(),
800 &[
801 (rel_path(""), false),
802 (rel_path(".gitignore"), false),
803 (rel_path("a"), false),
804 (rel_path("a/a.js"), false),
805 (rel_path("b"), false),
806 (rel_path("b/b.js"), false),
807 (rel_path("node_modules"), true),
808 (rel_path("node_modules/c"), true),
809 (rel_path("node_modules/d"), true),
810 (rel_path("node_modules/d/d.js"), true),
811 (rel_path("node_modules/d/e"), true),
812 (rel_path("node_modules/d/f"), true),
813 ]
814 );
815 });
816 let read_dir_count_2 = fs.read_dir_call_count();
817 assert_eq!(read_dir_count_2 - read_dir_count_1, 2);
818
819 // Update the gitignore so that node_modules is no longer ignored,
820 // but a subdirectory is ignored
821 fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default())
822 .await
823 .unwrap();
824 cx.executor().run_until_parked();
825
826 // All of the directories that are no longer ignored are now loaded.
827 tree.read_with(cx, |tree, _| {
828 assert_eq!(
829 tree.entries(true, 0)
830 .map(|e| (e.path.as_ref(), e.is_ignored))
831 .collect::<Vec<_>>(),
832 &[
833 (rel_path(""), false),
834 (rel_path(".gitignore"), false),
835 (rel_path("a"), false),
836 (rel_path("a/a.js"), false),
837 (rel_path("b"), false),
838 (rel_path("b/b.js"), false),
839 // This directory is no longer ignored
840 (rel_path("node_modules"), false),
841 (rel_path("node_modules/c"), false),
842 (rel_path("node_modules/c/c.js"), false),
843 (rel_path("node_modules/d"), false),
844 (rel_path("node_modules/d/d.js"), false),
845 // This subdirectory is now ignored
846 (rel_path("node_modules/d/e"), true),
847 (rel_path("node_modules/d/f"), false),
848 (rel_path("node_modules/d/f/f1.js"), false),
849 (rel_path("node_modules/d/f/f2.js"), false),
850 ]
851 );
852 });
853
854 // Each of the newly-loaded directories is scanned only once.
855 let read_dir_count_3 = fs.read_dir_call_count();
856 assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
857}
858
859#[gpui::test]
860async fn test_write_file(cx: &mut TestAppContext) {
861 init_test(cx);
862 cx.executor().allow_parking();
863 let dir = TempTree::new(json!({
864 ".git": {},
865 ".gitignore": "ignored-dir\n",
866 "tracked-dir": {},
867 "ignored-dir": {}
868 }));
869
870 let worktree = Worktree::local(
871 dir.path(),
872 true,
873 Arc::new(RealFs::new(None, cx.executor())),
874 Default::default(),
875 true,
876 WorktreeId::from_proto(0),
877 &mut cx.to_async(),
878 )
879 .await
880 .unwrap();
881
882 #[cfg(not(target_os = "macos"))]
883 fs::fs_watcher::global(|_| {}).unwrap();
884
885 cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
886 .await;
887 worktree.flush_fs_events(cx).await;
888
889 worktree
890 .update(cx, |tree, cx| {
891 tree.write_file(
892 rel_path("tracked-dir/file.txt").into(),
893 "hello".into(),
894 Default::default(),
895 encoding_rs::UTF_8,
896 false,
897 cx,
898 )
899 })
900 .await
901 .unwrap();
902 worktree
903 .update(cx, |tree, cx| {
904 tree.write_file(
905 rel_path("ignored-dir/file.txt").into(),
906 "world".into(),
907 Default::default(),
908 encoding_rs::UTF_8,
909 false,
910 cx,
911 )
912 })
913 .await
914 .unwrap();
915 worktree.read_with(cx, |tree, _| {
916 let tracked = tree
917 .entry_for_path(rel_path("tracked-dir/file.txt"))
918 .unwrap();
919 let ignored = tree
920 .entry_for_path(rel_path("ignored-dir/file.txt"))
921 .unwrap();
922 assert!(!tracked.is_ignored);
923 assert!(ignored.is_ignored);
924 });
925}
926
927#[gpui::test]
928async fn test_file_scan_inclusions(cx: &mut TestAppContext) {
929 init_test(cx);
930 cx.executor().allow_parking();
931 let dir = TempTree::new(json!({
932 ".gitignore": "**/target\n/node_modules\ntop_level.txt\n",
933 "target": {
934 "index": "blah2"
935 },
936 "node_modules": {
937 ".DS_Store": "",
938 "prettier": {
939 "package.json": "{}",
940 },
941 "package.json": "//package.json"
942 },
943 "src": {
944 ".DS_Store": "",
945 "foo": {
946 "foo.rs": "mod another;\n",
947 "another.rs": "// another",
948 },
949 "bar": {
950 "bar.rs": "// bar",
951 },
952 "lib.rs": "mod foo;\nmod bar;\n",
953 },
954 "top_level.txt": "top level file",
955 ".DS_Store": "",
956 }));
957 cx.update(|cx| {
958 cx.update_global::<SettingsStore, _>(|store, cx| {
959 store.update_user_settings(cx, |settings| {
960 settings.project.worktree.file_scan_exclusions = Some(vec![]);
961 settings.project.worktree.file_scan_inclusions = Some(vec![
962 "node_modules/**/package.json".to_string(),
963 "**/.DS_Store".to_string(),
964 ]);
965 });
966 });
967 });
968
969 let tree = Worktree::local(
970 dir.path(),
971 true,
972 Arc::new(RealFs::new(None, cx.executor())),
973 Default::default(),
974 true,
975 WorktreeId::from_proto(0),
976 &mut cx.to_async(),
977 )
978 .await
979 .unwrap();
980 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
981 .await;
982 tree.flush_fs_events(cx).await;
983 tree.read_with(cx, |tree, _| {
984 // Assert that file_scan_inclusions overrides file_scan_exclusions.
985 check_worktree_entries(
986 tree,
987 &[],
988 &["target", "node_modules"],
989 &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
990 &[
991 "node_modules/prettier/package.json",
992 ".DS_Store",
993 "node_modules/.DS_Store",
994 "src/.DS_Store",
995 ],
996 )
997 });
998}
999
1000#[gpui::test]
1001async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext) {
1002 init_test(cx);
1003 cx.executor().allow_parking();
1004 let dir = TempTree::new(json!({
1005 ".gitignore": "**/target\n/node_modules\n",
1006 "target": {
1007 "index": "blah2"
1008 },
1009 "node_modules": {
1010 ".DS_Store": "",
1011 "prettier": {
1012 "package.json": "{}",
1013 },
1014 },
1015 "src": {
1016 ".DS_Store": "",
1017 "foo": {
1018 "foo.rs": "mod another;\n",
1019 "another.rs": "// another",
1020 },
1021 },
1022 ".DS_Store": "",
1023 }));
1024
1025 cx.update(|cx| {
1026 cx.update_global::<SettingsStore, _>(|store, cx| {
1027 store.update_user_settings(cx, |settings| {
1028 settings.project.worktree.file_scan_exclusions =
1029 Some(vec!["**/.DS_Store".to_string()]);
1030 settings.project.worktree.file_scan_inclusions =
1031 Some(vec!["**/.DS_Store".to_string()]);
1032 });
1033 });
1034 });
1035
1036 let tree = Worktree::local(
1037 dir.path(),
1038 true,
1039 Arc::new(RealFs::new(None, cx.executor())),
1040 Default::default(),
1041 true,
1042 WorktreeId::from_proto(0),
1043 &mut cx.to_async(),
1044 )
1045 .await
1046 .unwrap();
1047 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1048 .await;
1049 tree.flush_fs_events(cx).await;
1050 tree.read_with(cx, |tree, _| {
1051 // Assert that file_scan_inclusions overrides file_scan_exclusions.
1052 check_worktree_entries(
1053 tree,
1054 &[".DS_Store, src/.DS_Store"],
1055 &["target", "node_modules"],
1056 &["src/foo/another.rs", "src/foo/foo.rs", ".gitignore"],
1057 &[],
1058 )
1059 });
1060}
1061
1062#[gpui::test]
1063async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppContext) {
1064 init_test(cx);
1065 cx.executor().allow_parking();
1066 let dir = TempTree::new(json!({
1067 ".gitignore": "**/target\n/node_modules/\n",
1068 "target": {
1069 "index": "blah2"
1070 },
1071 "node_modules": {
1072 ".DS_Store": "",
1073 "prettier": {
1074 "package.json": "{}",
1075 },
1076 },
1077 "src": {
1078 ".DS_Store": "",
1079 "foo": {
1080 "foo.rs": "mod another;\n",
1081 "another.rs": "// another",
1082 },
1083 },
1084 ".DS_Store": "",
1085 }));
1086
1087 cx.update(|cx| {
1088 cx.update_global::<SettingsStore, _>(|store, cx| {
1089 store.update_user_settings(cx, |settings| {
1090 settings.project.worktree.file_scan_exclusions = Some(vec![]);
1091 settings.project.worktree.file_scan_inclusions =
1092 Some(vec!["node_modules/**".to_string()]);
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 true,
1102 WorktreeId::from_proto(0),
1103 &mut cx.to_async(),
1104 )
1105 .await
1106 .unwrap();
1107 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1108 .await;
1109 tree.flush_fs_events(cx).await;
1110
1111 tree.read_with(cx, |tree, _| {
1112 assert!(
1113 tree.entry_for_path(rel_path("node_modules"))
1114 .is_some_and(|f| f.is_always_included)
1115 );
1116 assert!(
1117 tree.entry_for_path(rel_path("node_modules/prettier/package.json"))
1118 .is_some_and(|f| f.is_always_included)
1119 );
1120 });
1121
1122 cx.update(|cx| {
1123 cx.update_global::<SettingsStore, _>(|store, cx| {
1124 store.update_user_settings(cx, |settings| {
1125 settings.project.worktree.file_scan_exclusions = Some(vec![]);
1126 settings.project.worktree.file_scan_inclusions = Some(vec![]);
1127 });
1128 });
1129 });
1130 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1131 .await;
1132 tree.flush_fs_events(cx).await;
1133
1134 tree.read_with(cx, |tree, _| {
1135 assert!(
1136 tree.entry_for_path(rel_path("node_modules"))
1137 .is_some_and(|f| !f.is_always_included)
1138 );
1139 assert!(
1140 tree.entry_for_path(rel_path("node_modules/prettier/package.json"))
1141 .is_some_and(|f| !f.is_always_included)
1142 );
1143 });
1144}
1145
1146#[gpui::test]
1147async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
1148 init_test(cx);
1149 cx.executor().allow_parking();
1150 let dir = TempTree::new(json!({
1151 ".gitignore": "**/target\n/node_modules\n",
1152 "target": {
1153 "index": "blah2"
1154 },
1155 "node_modules": {
1156 ".DS_Store": "",
1157 "prettier": {
1158 "package.json": "{}",
1159 },
1160 },
1161 "src": {
1162 ".DS_Store": "",
1163 "foo": {
1164 "foo.rs": "mod another;\n",
1165 "another.rs": "// another",
1166 },
1167 "bar": {
1168 "bar.rs": "// bar",
1169 },
1170 "lib.rs": "mod foo;\nmod bar;\n",
1171 },
1172 ".DS_Store": "",
1173 }));
1174 cx.update(|cx| {
1175 cx.update_global::<SettingsStore, _>(|store, cx| {
1176 store.update_user_settings(cx, |settings| {
1177 settings.project.worktree.file_scan_exclusions =
1178 Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
1179 });
1180 });
1181 });
1182
1183 let tree = Worktree::local(
1184 dir.path(),
1185 true,
1186 Arc::new(RealFs::new(None, cx.executor())),
1187 Default::default(),
1188 true,
1189 WorktreeId::from_proto(0),
1190 &mut cx.to_async(),
1191 )
1192 .await
1193 .unwrap();
1194 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1195 .await;
1196 tree.flush_fs_events(cx).await;
1197 tree.read_with(cx, |tree, _| {
1198 check_worktree_entries(
1199 tree,
1200 &[
1201 "src/foo/foo.rs",
1202 "src/foo/another.rs",
1203 "node_modules/.DS_Store",
1204 "src/.DS_Store",
1205 ".DS_Store",
1206 ],
1207 &["target", "node_modules"],
1208 &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
1209 &[],
1210 )
1211 });
1212
1213 cx.update(|cx| {
1214 cx.update_global::<SettingsStore, _>(|store, cx| {
1215 store.update_user_settings(cx, |settings| {
1216 settings.project.worktree.file_scan_exclusions =
1217 Some(vec!["**/node_modules/**".to_string()]);
1218 });
1219 });
1220 });
1221 tree.flush_fs_events(cx).await;
1222 cx.executor().run_until_parked();
1223 tree.read_with(cx, |tree, _| {
1224 check_worktree_entries(
1225 tree,
1226 &[
1227 "node_modules/prettier/package.json",
1228 "node_modules/.DS_Store",
1229 "node_modules",
1230 ],
1231 &["target"],
1232 &[
1233 ".gitignore",
1234 "src/lib.rs",
1235 "src/bar/bar.rs",
1236 "src/foo/foo.rs",
1237 "src/foo/another.rs",
1238 "src/.DS_Store",
1239 ".DS_Store",
1240 ],
1241 &[],
1242 )
1243 });
1244}
1245
1246#[gpui::test]
1247async fn test_hidden_files(cx: &mut TestAppContext) {
1248 init_test(cx);
1249 cx.executor().allow_parking();
1250 let dir = TempTree::new(json!({
1251 ".gitignore": "**/target\n",
1252 ".hidden_file": "content",
1253 ".hidden_dir": {
1254 "nested.rs": "code",
1255 },
1256 "src": {
1257 "visible.rs": "code",
1258 },
1259 "logs": {
1260 "app.log": "logs",
1261 "debug.log": "logs",
1262 },
1263 "visible.txt": "content",
1264 }));
1265
1266 let tree = Worktree::local(
1267 dir.path(),
1268 true,
1269 Arc::new(RealFs::new(None, cx.executor())),
1270 Default::default(),
1271 true,
1272 WorktreeId::from_proto(0),
1273 &mut cx.to_async(),
1274 )
1275 .await
1276 .unwrap();
1277 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1278 .await;
1279 tree.flush_fs_events(cx).await;
1280
1281 tree.read_with(cx, |tree, _| {
1282 assert_eq!(
1283 tree.entries(true, 0)
1284 .map(|entry| (entry.path.as_ref(), entry.is_hidden))
1285 .collect::<Vec<_>>(),
1286 vec![
1287 (rel_path(""), false),
1288 (rel_path(".gitignore"), true),
1289 (rel_path(".hidden_dir"), true),
1290 (rel_path(".hidden_dir/nested.rs"), true),
1291 (rel_path(".hidden_file"), true),
1292 (rel_path("logs"), false),
1293 (rel_path("logs/app.log"), false),
1294 (rel_path("logs/debug.log"), false),
1295 (rel_path("src"), false),
1296 (rel_path("src/visible.rs"), false),
1297 (rel_path("visible.txt"), false),
1298 ]
1299 );
1300 });
1301
1302 cx.update(|cx| {
1303 cx.update_global::<SettingsStore, _>(|store, cx| {
1304 store.update_user_settings(cx, |settings| {
1305 settings.project.worktree.hidden_files = Some(vec!["**/*.log".to_string()]);
1306 });
1307 });
1308 });
1309 tree.flush_fs_events(cx).await;
1310 cx.executor().run_until_parked();
1311
1312 tree.read_with(cx, |tree, _| {
1313 assert_eq!(
1314 tree.entries(true, 0)
1315 .map(|entry| (entry.path.as_ref(), entry.is_hidden))
1316 .collect::<Vec<_>>(),
1317 vec![
1318 (rel_path(""), false),
1319 (rel_path(".gitignore"), false),
1320 (rel_path(".hidden_dir"), false),
1321 (rel_path(".hidden_dir/nested.rs"), false),
1322 (rel_path(".hidden_file"), false),
1323 (rel_path("logs"), false),
1324 (rel_path("logs/app.log"), true),
1325 (rel_path("logs/debug.log"), true),
1326 (rel_path("src"), false),
1327 (rel_path("src/visible.rs"), false),
1328 (rel_path("visible.txt"), false),
1329 ]
1330 );
1331 });
1332}
1333
1334#[gpui::test]
1335async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
1336 init_test(cx);
1337 cx.executor().allow_parking();
1338 let dir = TempTree::new(json!({
1339 ".git": {
1340 "HEAD": "ref: refs/heads/main\n",
1341 "foo": "bar",
1342 },
1343 ".gitignore": "**/target\n/node_modules\ntest_output\n",
1344 "target": {
1345 "index": "blah2"
1346 },
1347 "node_modules": {
1348 ".DS_Store": "",
1349 "prettier": {
1350 "package.json": "{}",
1351 },
1352 },
1353 "src": {
1354 ".DS_Store": "",
1355 "foo": {
1356 "foo.rs": "mod another;\n",
1357 "another.rs": "// another",
1358 },
1359 "bar": {
1360 "bar.rs": "// bar",
1361 },
1362 "lib.rs": "mod foo;\nmod bar;\n",
1363 },
1364 ".DS_Store": "",
1365 }));
1366 cx.update(|cx| {
1367 cx.update_global::<SettingsStore, _>(|store, cx| {
1368 store.update_user_settings(cx, |settings| {
1369 settings.project.worktree.file_scan_exclusions = Some(vec![
1370 "**/.git".to_string(),
1371 "node_modules/".to_string(),
1372 "build_output".to_string(),
1373 ]);
1374 });
1375 });
1376 });
1377
1378 let tree = Worktree::local(
1379 dir.path(),
1380 true,
1381 Arc::new(RealFs::new(None, cx.executor())),
1382 Default::default(),
1383 true,
1384 WorktreeId::from_proto(0),
1385 &mut cx.to_async(),
1386 )
1387 .await
1388 .unwrap();
1389 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1390 .await;
1391 tree.flush_fs_events(cx).await;
1392 tree.read_with(cx, |tree, _| {
1393 check_worktree_entries(
1394 tree,
1395 &[
1396 ".git/HEAD",
1397 ".git/foo",
1398 "node_modules",
1399 "node_modules/.DS_Store",
1400 "node_modules/prettier",
1401 "node_modules/prettier/package.json",
1402 ],
1403 &["target"],
1404 &[
1405 ".DS_Store",
1406 "src/.DS_Store",
1407 "src/lib.rs",
1408 "src/foo/foo.rs",
1409 "src/foo/another.rs",
1410 "src/bar/bar.rs",
1411 ".gitignore",
1412 ],
1413 &[],
1414 )
1415 });
1416
1417 let new_excluded_dir = dir.path().join("build_output");
1418 let new_ignored_dir = dir.path().join("test_output");
1419 std::fs::create_dir_all(&new_excluded_dir)
1420 .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
1421 std::fs::create_dir_all(&new_ignored_dir)
1422 .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
1423 let node_modules_dir = dir.path().join("node_modules");
1424 let dot_git_dir = dir.path().join(".git");
1425 let src_dir = dir.path().join("src");
1426 for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
1427 assert!(
1428 existing_dir.is_dir(),
1429 "Expect {existing_dir:?} to be present in the FS already"
1430 );
1431 }
1432
1433 for directory_for_new_file in [
1434 new_excluded_dir,
1435 new_ignored_dir,
1436 node_modules_dir,
1437 dot_git_dir,
1438 src_dir,
1439 ] {
1440 std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
1441 .unwrap_or_else(|e| {
1442 panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
1443 });
1444 }
1445 tree.flush_fs_events(cx).await;
1446
1447 tree.read_with(cx, |tree, _| {
1448 check_worktree_entries(
1449 tree,
1450 &[
1451 ".git/HEAD",
1452 ".git/foo",
1453 ".git/new_file",
1454 "node_modules",
1455 "node_modules/.DS_Store",
1456 "node_modules/prettier",
1457 "node_modules/prettier/package.json",
1458 "node_modules/new_file",
1459 "build_output",
1460 "build_output/new_file",
1461 "test_output/new_file",
1462 ],
1463 &["target", "test_output"],
1464 &[
1465 ".DS_Store",
1466 "src/.DS_Store",
1467 "src/lib.rs",
1468 "src/foo/foo.rs",
1469 "src/foo/another.rs",
1470 "src/bar/bar.rs",
1471 "src/new_file",
1472 ".gitignore",
1473 ],
1474 &[],
1475 )
1476 });
1477}
1478
1479#[gpui::test]
1480async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
1481 init_test(cx);
1482 cx.executor().allow_parking();
1483 let dir = TempTree::new(json!({
1484 ".git": {
1485 "HEAD": "ref: refs/heads/main\n",
1486 "foo": "foo contents",
1487 },
1488 }));
1489 let dot_git_worktree_dir = dir.path().join(".git");
1490
1491 let tree = Worktree::local(
1492 dot_git_worktree_dir.clone(),
1493 true,
1494 Arc::new(RealFs::new(None, cx.executor())),
1495 Default::default(),
1496 true,
1497 WorktreeId::from_proto(0),
1498 &mut cx.to_async(),
1499 )
1500 .await
1501 .unwrap();
1502 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1503 .await;
1504 tree.flush_fs_events(cx).await;
1505 tree.read_with(cx, |tree, _| {
1506 check_worktree_entries(tree, &[], &["HEAD", "foo"], &[], &[])
1507 });
1508
1509 std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
1510 .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
1511 tree.flush_fs_events(cx).await;
1512 tree.read_with(cx, |tree, _| {
1513 check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[], &[])
1514 });
1515}
1516
1517#[gpui::test(iterations = 30)]
1518async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
1519 init_test(cx);
1520 let fs = FakeFs::new(cx.background_executor.clone());
1521 fs.insert_tree(
1522 "/root",
1523 json!({
1524 "b": {},
1525 "c": {},
1526 "d": {},
1527 }),
1528 )
1529 .await;
1530
1531 let tree = Worktree::local(
1532 "/root".as_ref(),
1533 true,
1534 fs,
1535 Default::default(),
1536 true,
1537 WorktreeId::from_proto(0),
1538 &mut cx.to_async(),
1539 )
1540 .await
1541 .unwrap();
1542
1543 let snapshot1 = tree.update(cx, |tree, cx| {
1544 let tree = tree.as_local_mut().unwrap();
1545 let snapshot = Arc::new(Mutex::new(tree.snapshot()));
1546 tree.observe_updates(0, cx, {
1547 let snapshot = snapshot.clone();
1548 let settings = tree.settings();
1549 move |update| {
1550 snapshot
1551 .lock()
1552 .apply_remote_update(update, &settings.file_scan_inclusions);
1553 async { true }
1554 }
1555 });
1556 snapshot
1557 });
1558
1559 let entry = tree
1560 .update(cx, |tree, cx| {
1561 tree.as_local_mut()
1562 .unwrap()
1563 .create_entry(rel_path("a/e").into(), true, None, cx)
1564 })
1565 .await
1566 .unwrap()
1567 .into_included()
1568 .unwrap();
1569 assert!(entry.is_dir());
1570
1571 cx.executor().run_until_parked();
1572 tree.read_with(cx, |tree, _| {
1573 assert_eq!(
1574 tree.entry_for_path(rel_path("a/e")).unwrap().kind,
1575 EntryKind::Dir
1576 );
1577 });
1578
1579 let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1580 assert_eq!(
1581 snapshot1.lock().entries(true, 0).collect::<Vec<_>>(),
1582 snapshot2.entries(true, 0).collect::<Vec<_>>()
1583 );
1584}
1585
1586#[gpui::test]
1587async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1588 init_test(cx);
1589 cx.executor().allow_parking();
1590
1591 let fs_fake = FakeFs::new(cx.background_executor.clone());
1592 fs_fake
1593 .insert_tree(
1594 "/root",
1595 json!({
1596 "a": {},
1597 }),
1598 )
1599 .await;
1600
1601 let tree_fake = Worktree::local(
1602 "/root".as_ref(),
1603 true,
1604 fs_fake,
1605 Default::default(),
1606 true,
1607 WorktreeId::from_proto(0),
1608 &mut cx.to_async(),
1609 )
1610 .await
1611 .unwrap();
1612
1613 let entry = tree_fake
1614 .update(cx, |tree, cx| {
1615 tree.as_local_mut().unwrap().create_entry(
1616 rel_path("a/b/c/d.txt").into(),
1617 false,
1618 None,
1619 cx,
1620 )
1621 })
1622 .await
1623 .unwrap()
1624 .into_included()
1625 .unwrap();
1626 assert!(entry.is_file());
1627
1628 cx.executor().run_until_parked();
1629 tree_fake.read_with(cx, |tree, _| {
1630 assert!(
1631 tree.entry_for_path(rel_path("a/b/c/d.txt"))
1632 .unwrap()
1633 .is_file()
1634 );
1635 assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir());
1636 assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir());
1637 });
1638
1639 let fs_real = Arc::new(RealFs::new(None, cx.executor()));
1640 let temp_root = TempTree::new(json!({
1641 "a": {}
1642 }));
1643
1644 let tree_real = Worktree::local(
1645 temp_root.path(),
1646 true,
1647 fs_real,
1648 Default::default(),
1649 true,
1650 WorktreeId::from_proto(0),
1651 &mut cx.to_async(),
1652 )
1653 .await
1654 .unwrap();
1655
1656 let entry = tree_real
1657 .update(cx, |tree, cx| {
1658 tree.as_local_mut().unwrap().create_entry(
1659 rel_path("a/b/c/d.txt").into(),
1660 false,
1661 None,
1662 cx,
1663 )
1664 })
1665 .await
1666 .unwrap()
1667 .into_included()
1668 .unwrap();
1669 assert!(entry.is_file());
1670
1671 cx.executor().run_until_parked();
1672 tree_real.read_with(cx, |tree, _| {
1673 assert!(
1674 tree.entry_for_path(rel_path("a/b/c/d.txt"))
1675 .unwrap()
1676 .is_file()
1677 );
1678 assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir());
1679 assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir());
1680 });
1681
1682 // Test smallest change
1683 let entry = tree_real
1684 .update(cx, |tree, cx| {
1685 tree.as_local_mut().unwrap().create_entry(
1686 rel_path("a/b/c/e.txt").into(),
1687 false,
1688 None,
1689 cx,
1690 )
1691 })
1692 .await
1693 .unwrap()
1694 .into_included()
1695 .unwrap();
1696 assert!(entry.is_file());
1697
1698 cx.executor().run_until_parked();
1699 tree_real.read_with(cx, |tree, _| {
1700 assert!(
1701 tree.entry_for_path(rel_path("a/b/c/e.txt"))
1702 .unwrap()
1703 .is_file()
1704 );
1705 });
1706
1707 // Test largest change
1708 let entry = tree_real
1709 .update(cx, |tree, cx| {
1710 tree.as_local_mut().unwrap().create_entry(
1711 rel_path("d/e/f/g.txt").into(),
1712 false,
1713 None,
1714 cx,
1715 )
1716 })
1717 .await
1718 .unwrap()
1719 .into_included()
1720 .unwrap();
1721 assert!(entry.is_file());
1722
1723 cx.executor().run_until_parked();
1724 tree_real.read_with(cx, |tree, _| {
1725 assert!(
1726 tree.entry_for_path(rel_path("d/e/f/g.txt"))
1727 .unwrap()
1728 .is_file()
1729 );
1730 assert!(tree.entry_for_path(rel_path("d/e/f")).unwrap().is_dir());
1731 assert!(tree.entry_for_path(rel_path("d/e")).unwrap().is_dir());
1732 assert!(tree.entry_for_path(rel_path("d")).unwrap().is_dir());
1733 });
1734}
1735
1736#[gpui::test]
1737async fn test_create_file_in_expanded_gitignored_dir(cx: &mut TestAppContext) {
1738 // Tests the behavior of our worktree refresh when a file in a gitignored directory
1739 // is created.
1740 init_test(cx);
1741 let fs = FakeFs::new(cx.background_executor.clone());
1742 fs.insert_tree(
1743 "/root",
1744 json!({
1745 ".gitignore": "ignored_dir\n",
1746 "ignored_dir": {
1747 "existing_file.txt": "existing content",
1748 "another_file.txt": "another content",
1749 },
1750 }),
1751 )
1752 .await;
1753
1754 let tree = Worktree::local(
1755 Path::new("/root"),
1756 true,
1757 fs.clone(),
1758 Default::default(),
1759 true,
1760 WorktreeId::from_proto(0),
1761 &mut cx.to_async(),
1762 )
1763 .await
1764 .unwrap();
1765
1766 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1767 .await;
1768
1769 tree.read_with(cx, |tree, _| {
1770 let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
1771 assert!(ignored_dir.is_ignored);
1772 assert_eq!(ignored_dir.kind, EntryKind::UnloadedDir);
1773 });
1774
1775 tree.update(cx, |tree, cx| {
1776 tree.load_file(rel_path("ignored_dir/existing_file.txt"), cx)
1777 })
1778 .await
1779 .unwrap();
1780
1781 tree.read_with(cx, |tree, _| {
1782 let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
1783 assert!(ignored_dir.is_ignored);
1784 assert_eq!(ignored_dir.kind, EntryKind::Dir);
1785
1786 assert!(
1787 tree.entry_for_path(rel_path("ignored_dir/existing_file.txt"))
1788 .is_some()
1789 );
1790 assert!(
1791 tree.entry_for_path(rel_path("ignored_dir/another_file.txt"))
1792 .is_some()
1793 );
1794 });
1795
1796 let entry = tree
1797 .update(cx, |tree, cx| {
1798 tree.create_entry(rel_path("ignored_dir/new_file.txt").into(), false, None, cx)
1799 })
1800 .await
1801 .unwrap();
1802 assert!(entry.into_included().is_some());
1803
1804 cx.executor().run_until_parked();
1805
1806 tree.read_with(cx, |tree, _| {
1807 let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
1808 assert!(ignored_dir.is_ignored);
1809 assert_eq!(
1810 ignored_dir.kind,
1811 EntryKind::Dir,
1812 "ignored_dir should still be loaded, not UnloadedDir"
1813 );
1814
1815 assert!(
1816 tree.entry_for_path(rel_path("ignored_dir/existing_file.txt"))
1817 .is_some(),
1818 "existing_file.txt should still be visible"
1819 );
1820 assert!(
1821 tree.entry_for_path(rel_path("ignored_dir/another_file.txt"))
1822 .is_some(),
1823 "another_file.txt should still be visible"
1824 );
1825 assert!(
1826 tree.entry_for_path(rel_path("ignored_dir/new_file.txt"))
1827 .is_some(),
1828 "new_file.txt should be visible"
1829 );
1830 });
1831}
1832
1833#[gpui::test]
1834async fn test_fs_event_for_gitignored_dir_does_not_lose_contents(cx: &mut TestAppContext) {
1835 // Tests the behavior of our worktree refresh when a directory modification for a gitignored directory
1836 // is triggered.
1837 init_test(cx);
1838 let fs = FakeFs::new(cx.background_executor.clone());
1839 fs.insert_tree(
1840 "/root",
1841 json!({
1842 ".gitignore": "ignored_dir\n",
1843 "ignored_dir": {
1844 "file1.txt": "content1",
1845 "file2.txt": "content2",
1846 },
1847 }),
1848 )
1849 .await;
1850
1851 let tree = Worktree::local(
1852 Path::new("/root"),
1853 true,
1854 fs.clone(),
1855 Default::default(),
1856 true,
1857 WorktreeId::from_proto(0),
1858 &mut cx.to_async(),
1859 )
1860 .await
1861 .unwrap();
1862
1863 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1864 .await;
1865
1866 // Load a file to expand the ignored directory
1867 tree.update(cx, |tree, cx| {
1868 tree.load_file(rel_path("ignored_dir/file1.txt"), cx)
1869 })
1870 .await
1871 .unwrap();
1872
1873 tree.read_with(cx, |tree, _| {
1874 let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
1875 assert_eq!(ignored_dir.kind, EntryKind::Dir);
1876 assert!(
1877 tree.entry_for_path(rel_path("ignored_dir/file1.txt"))
1878 .is_some()
1879 );
1880 assert!(
1881 tree.entry_for_path(rel_path("ignored_dir/file2.txt"))
1882 .is_some()
1883 );
1884 });
1885
1886 fs.emit_fs_event("/root/ignored_dir", Some(fs::PathEventKind::Changed));
1887 tree.flush_fs_events(cx).await;
1888
1889 tree.read_with(cx, |tree, _| {
1890 let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
1891 assert_eq!(
1892 ignored_dir.kind,
1893 EntryKind::Dir,
1894 "ignored_dir should still be loaded (Dir), not UnloadedDir"
1895 );
1896 assert!(
1897 tree.entry_for_path(rel_path("ignored_dir/file1.txt"))
1898 .is_some(),
1899 "file1.txt should still be visible after directory fs event"
1900 );
1901 assert!(
1902 tree.entry_for_path(rel_path("ignored_dir/file2.txt"))
1903 .is_some(),
1904 "file2.txt should still be visible after directory fs event"
1905 );
1906 });
1907}
1908
1909#[gpui::test(iterations = 100)]
1910async fn test_random_worktree_operations_during_initial_scan(
1911 cx: &mut TestAppContext,
1912 mut rng: StdRng,
1913) {
1914 init_test(cx);
1915 let operations = env::var("OPERATIONS")
1916 .map(|o| o.parse().unwrap())
1917 .unwrap_or(5);
1918 let initial_entries = env::var("INITIAL_ENTRIES")
1919 .map(|o| o.parse().unwrap())
1920 .unwrap_or(20);
1921
1922 let root_dir = Path::new(path!("/test"));
1923 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1924 fs.as_fake().insert_tree(root_dir, json!({})).await;
1925 for _ in 0..initial_entries {
1926 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1927 }
1928 log::info!("generated initial tree");
1929
1930 let worktree = Worktree::local(
1931 root_dir,
1932 true,
1933 fs.clone(),
1934 Default::default(),
1935 true,
1936 WorktreeId::from_proto(0),
1937 &mut cx.to_async(),
1938 )
1939 .await
1940 .unwrap();
1941
1942 let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1943 let updates = Arc::new(Mutex::new(Vec::new()));
1944 worktree.update(cx, |tree, cx| {
1945 check_worktree_change_events(tree, cx);
1946
1947 tree.as_local_mut().unwrap().observe_updates(0, cx, {
1948 let updates = updates.clone();
1949 move |update| {
1950 updates.lock().push(update);
1951 async { true }
1952 }
1953 });
1954 });
1955
1956 for _ in 0..operations {
1957 worktree
1958 .update(cx, |worktree, cx| {
1959 randomly_mutate_worktree(worktree, &mut rng, cx)
1960 })
1961 .await
1962 .log_err();
1963 worktree.read_with(cx, |tree, _| {
1964 tree.as_local().unwrap().snapshot().check_invariants(true)
1965 });
1966
1967 if rng.random_bool(0.6) {
1968 snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1969 }
1970 }
1971
1972 worktree
1973 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1974 .await;
1975
1976 cx.executor().run_until_parked();
1977
1978 let final_snapshot = worktree.read_with(cx, |tree, _| {
1979 let tree = tree.as_local().unwrap();
1980 let snapshot = tree.snapshot();
1981 snapshot.check_invariants(true);
1982 snapshot
1983 });
1984
1985 let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1986
1987 for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1988 let mut updated_snapshot = snapshot.clone();
1989 for update in updates.lock().iter() {
1990 if update.scan_id >= updated_snapshot.scan_id() as u64 {
1991 updated_snapshot
1992 .apply_remote_update(update.clone(), &settings.file_scan_inclusions);
1993 }
1994 }
1995
1996 assert_eq!(
1997 updated_snapshot.entries(true, 0).collect::<Vec<_>>(),
1998 final_snapshot.entries(true, 0).collect::<Vec<_>>(),
1999 "wrong updates after snapshot {i}: {updates:#?}",
2000 );
2001 }
2002}
2003
2004#[gpui::test(iterations = 100)]
2005async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
2006 init_test(cx);
2007 let operations = env::var("OPERATIONS")
2008 .map(|o| o.parse().unwrap())
2009 .unwrap_or(40);
2010 let initial_entries = env::var("INITIAL_ENTRIES")
2011 .map(|o| o.parse().unwrap())
2012 .unwrap_or(20);
2013
2014 let root_dir = Path::new(path!("/test"));
2015 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
2016 fs.as_fake().insert_tree(root_dir, json!({})).await;
2017 for _ in 0..initial_entries {
2018 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
2019 }
2020 log::info!("generated initial tree");
2021
2022 let worktree = Worktree::local(
2023 root_dir,
2024 true,
2025 fs.clone(),
2026 Default::default(),
2027 true,
2028 WorktreeId::from_proto(0),
2029 &mut cx.to_async(),
2030 )
2031 .await
2032 .unwrap();
2033
2034 let updates = Arc::new(Mutex::new(Vec::new()));
2035 worktree.update(cx, |tree, cx| {
2036 check_worktree_change_events(tree, cx);
2037
2038 tree.as_local_mut().unwrap().observe_updates(0, cx, {
2039 let updates = updates.clone();
2040 move |update| {
2041 updates.lock().push(update);
2042 async { true }
2043 }
2044 });
2045 });
2046
2047 worktree
2048 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
2049 .await;
2050
2051 fs.as_fake().pause_events();
2052 let mut snapshots = Vec::new();
2053 let mut mutations_len = operations;
2054 while mutations_len > 1 {
2055 if rng.random_bool(0.2) {
2056 worktree
2057 .update(cx, |worktree, cx| {
2058 randomly_mutate_worktree(worktree, &mut rng, cx)
2059 })
2060 .await
2061 .log_err();
2062 } else {
2063 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
2064 }
2065
2066 let buffered_event_count = fs.as_fake().buffered_event_count();
2067 if buffered_event_count > 0 && rng.random_bool(0.3) {
2068 let len = rng.random_range(0..=buffered_event_count);
2069 log::info!("flushing {} events", len);
2070 fs.as_fake().flush_events(len);
2071 } else {
2072 randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
2073 mutations_len -= 1;
2074 }
2075
2076 cx.executor().run_until_parked();
2077 if rng.random_bool(0.2) {
2078 log::info!("storing snapshot {}", snapshots.len());
2079 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
2080 snapshots.push(snapshot);
2081 }
2082 }
2083
2084 log::info!("quiescing");
2085 fs.as_fake().flush_events(usize::MAX);
2086 cx.executor().run_until_parked();
2087
2088 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
2089 snapshot.check_invariants(true);
2090 let expanded_paths = snapshot
2091 .expanded_entries()
2092 .map(|e| e.path.clone())
2093 .collect::<Vec<_>>();
2094
2095 {
2096 let new_worktree = Worktree::local(
2097 root_dir,
2098 true,
2099 fs.clone(),
2100 Default::default(),
2101 true,
2102 WorktreeId::from_proto(0),
2103 &mut cx.to_async(),
2104 )
2105 .await
2106 .unwrap();
2107 new_worktree
2108 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
2109 .await;
2110 new_worktree
2111 .update(cx, |tree, _| {
2112 tree.as_local_mut()
2113 .unwrap()
2114 .refresh_entries_for_paths(expanded_paths)
2115 })
2116 .recv()
2117 .await;
2118 let new_snapshot =
2119 new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
2120 assert_eq!(
2121 snapshot.entries_without_ids(true),
2122 new_snapshot.entries_without_ids(true)
2123 );
2124 }
2125
2126 let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
2127
2128 for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
2129 for update in updates.lock().iter() {
2130 if update.scan_id >= prev_snapshot.scan_id() as u64 {
2131 prev_snapshot.apply_remote_update(update.clone(), &settings.file_scan_inclusions);
2132 }
2133 }
2134
2135 assert_eq!(
2136 prev_snapshot
2137 .entries(true, 0)
2138 .map(ignore_pending_dir)
2139 .collect::<Vec<_>>(),
2140 snapshot
2141 .entries(true, 0)
2142 .map(ignore_pending_dir)
2143 .collect::<Vec<_>>(),
2144 "wrong updates after snapshot {i}: {updates:#?}",
2145 );
2146 }
2147
2148 fn ignore_pending_dir(entry: &Entry) -> Entry {
2149 let mut entry = entry.clone();
2150 if entry.kind.is_dir() {
2151 entry.kind = EntryKind::Dir
2152 }
2153 entry
2154 }
2155}
2156
2157// The worktree's `UpdatedEntries` event can be used to follow along with
2158// all changes to the worktree's snapshot.
2159fn check_worktree_change_events(tree: &mut Worktree, cx: &mut Context<Worktree>) {
2160 let mut entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
2161 cx.subscribe(&cx.entity(), move |tree, _, event, _| {
2162 if let Event::UpdatedEntries(changes) = event {
2163 for (path, _, change_type) in changes.iter() {
2164 let entry = tree.entry_for_path(path).cloned();
2165 let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
2166 Ok(ix) | Err(ix) => ix,
2167 };
2168 match change_type {
2169 PathChange::Added => entries.insert(ix, entry.unwrap()),
2170 PathChange::Removed => drop(entries.remove(ix)),
2171 PathChange::Updated => {
2172 let entry = entry.unwrap();
2173 let existing_entry = entries.get_mut(ix).unwrap();
2174 assert_eq!(existing_entry.path, entry.path);
2175 *existing_entry = entry;
2176 }
2177 PathChange::AddedOrUpdated | PathChange::Loaded => {
2178 let entry = entry.unwrap();
2179 if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
2180 *entries.get_mut(ix).unwrap() = entry;
2181 } else {
2182 entries.insert(ix, entry);
2183 }
2184 }
2185 }
2186 }
2187
2188 let new_entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
2189 assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
2190 }
2191 })
2192 .detach();
2193}
2194
2195fn randomly_mutate_worktree(
2196 worktree: &mut Worktree,
2197 rng: &mut impl Rng,
2198 cx: &mut Context<Worktree>,
2199) -> Task<Result<()>> {
2200 log::info!("mutating worktree");
2201 let worktree = worktree.as_local_mut().unwrap();
2202 let snapshot = worktree.snapshot();
2203 let entry = snapshot.entries(false, 0).choose(rng).unwrap();
2204
2205 match rng.random_range(0_u32..100) {
2206 0..=33 if entry.path.as_ref() != RelPath::empty() => {
2207 log::info!("deleting entry {:?} ({})", entry.path, entry.id.to_usize());
2208 worktree.delete_entry(entry.id, false, cx).unwrap()
2209 }
2210 _ => {
2211 if entry.is_dir() {
2212 let child_path = entry.path.join(rel_path(&random_filename(rng)));
2213 let is_dir = rng.random_bool(0.3);
2214 log::info!(
2215 "creating {} at {:?}",
2216 if is_dir { "dir" } else { "file" },
2217 child_path,
2218 );
2219 let task = worktree.create_entry(child_path, is_dir, None, cx);
2220 cx.background_spawn(async move {
2221 task.await?;
2222 Ok(())
2223 })
2224 } else {
2225 log::info!(
2226 "overwriting file {:?} ({})",
2227 &entry.path,
2228 entry.id.to_usize()
2229 );
2230 let task = worktree.write_file(
2231 entry.path.clone(),
2232 "".into(),
2233 Default::default(),
2234 encoding_rs::UTF_8,
2235 false,
2236 cx,
2237 );
2238 cx.background_spawn(async move {
2239 task.await?;
2240 Ok(())
2241 })
2242 }
2243 }
2244 }
2245}
2246
2247async fn randomly_mutate_fs(
2248 fs: &Arc<dyn Fs>,
2249 root_path: &Path,
2250 insertion_probability: f64,
2251 rng: &mut impl Rng,
2252) {
2253 log::info!("mutating fs");
2254 let mut files = Vec::new();
2255 let mut dirs = Vec::new();
2256 for path in fs.as_fake().paths(false) {
2257 if path.starts_with(root_path) {
2258 if fs.is_file(&path).await {
2259 files.push(path);
2260 } else {
2261 dirs.push(path);
2262 }
2263 }
2264 }
2265
2266 if (files.is_empty() && dirs.len() == 1) || rng.random_bool(insertion_probability) {
2267 let path = dirs.choose(rng).unwrap();
2268 let new_path = path.join(random_filename(rng));
2269
2270 if rng.random() {
2271 log::info!(
2272 "creating dir {:?}",
2273 new_path.strip_prefix(root_path).unwrap()
2274 );
2275 fs.create_dir(&new_path).await.unwrap();
2276 } else {
2277 log::info!(
2278 "creating file {:?}",
2279 new_path.strip_prefix(root_path).unwrap()
2280 );
2281 fs.create_file(&new_path, Default::default()).await.unwrap();
2282 }
2283 } else if rng.random_bool(0.05) {
2284 let ignore_dir_path = dirs.choose(rng).unwrap();
2285 let ignore_path = ignore_dir_path.join(GITIGNORE);
2286
2287 let subdirs = dirs
2288 .iter()
2289 .filter(|d| d.starts_with(ignore_dir_path))
2290 .cloned()
2291 .collect::<Vec<_>>();
2292 let subfiles = files
2293 .iter()
2294 .filter(|d| d.starts_with(ignore_dir_path))
2295 .cloned()
2296 .collect::<Vec<_>>();
2297 let files_to_ignore = {
2298 let len = rng.random_range(0..=subfiles.len());
2299 subfiles.choose_multiple(rng, len)
2300 };
2301 let dirs_to_ignore = {
2302 let len = rng.random_range(0..subdirs.len());
2303 subdirs.choose_multiple(rng, len)
2304 };
2305
2306 let mut ignore_contents = String::new();
2307 for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
2308 writeln!(
2309 ignore_contents,
2310 "{}",
2311 path_to_ignore
2312 .strip_prefix(ignore_dir_path)
2313 .unwrap()
2314 .to_str()
2315 .unwrap()
2316 )
2317 .unwrap();
2318 }
2319 log::info!(
2320 "creating gitignore {:?} with contents:\n{}",
2321 ignore_path.strip_prefix(root_path).unwrap(),
2322 ignore_contents
2323 );
2324 fs.save(
2325 &ignore_path,
2326 &ignore_contents.as_str().into(),
2327 Default::default(),
2328 )
2329 .await
2330 .unwrap();
2331 } else {
2332 let old_path = {
2333 let file_path = files.choose(rng);
2334 let dir_path = dirs[1..].choose(rng);
2335 file_path.into_iter().chain(dir_path).choose(rng).unwrap()
2336 };
2337
2338 let is_rename = rng.random();
2339 if is_rename {
2340 let new_path_parent = dirs
2341 .iter()
2342 .filter(|d| !d.starts_with(old_path))
2343 .choose(rng)
2344 .unwrap();
2345
2346 let overwrite_existing_dir =
2347 !old_path.starts_with(new_path_parent) && rng.random_bool(0.3);
2348 let new_path = if overwrite_existing_dir {
2349 fs.remove_dir(
2350 new_path_parent,
2351 RemoveOptions {
2352 recursive: true,
2353 ignore_if_not_exists: true,
2354 },
2355 )
2356 .await
2357 .unwrap();
2358 new_path_parent.to_path_buf()
2359 } else {
2360 new_path_parent.join(random_filename(rng))
2361 };
2362
2363 log::info!(
2364 "renaming {:?} to {}{:?}",
2365 old_path.strip_prefix(root_path).unwrap(),
2366 if overwrite_existing_dir {
2367 "overwrite "
2368 } else {
2369 ""
2370 },
2371 new_path.strip_prefix(root_path).unwrap()
2372 );
2373 fs.rename(
2374 old_path,
2375 &new_path,
2376 fs::RenameOptions {
2377 overwrite: true,
2378 ignore_if_exists: true,
2379 create_parents: false,
2380 },
2381 )
2382 .await
2383 .unwrap();
2384 } else if fs.is_file(old_path).await {
2385 log::info!(
2386 "deleting file {:?}",
2387 old_path.strip_prefix(root_path).unwrap()
2388 );
2389 fs.remove_file(old_path, Default::default()).await.unwrap();
2390 } else {
2391 log::info!(
2392 "deleting dir {:?}",
2393 old_path.strip_prefix(root_path).unwrap()
2394 );
2395 fs.remove_dir(
2396 old_path,
2397 RemoveOptions {
2398 recursive: true,
2399 ignore_if_not_exists: true,
2400 },
2401 )
2402 .await
2403 .unwrap();
2404 }
2405 }
2406}
2407
2408fn random_filename(rng: &mut impl Rng) -> String {
2409 (0..6)
2410 .map(|_| rng.sample(rand::distr::Alphanumeric))
2411 .map(char::from)
2412 .collect()
2413}
2414
2415#[gpui::test]
2416async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
2417 init_test(cx);
2418 let fs = FakeFs::new(cx.background_executor.clone());
2419 fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
2420 .await;
2421 let tree = Worktree::local(
2422 Path::new("/.env"),
2423 true,
2424 fs.clone(),
2425 Default::default(),
2426 true,
2427 WorktreeId::from_proto(0),
2428 &mut cx.to_async(),
2429 )
2430 .await
2431 .unwrap();
2432 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2433 .await;
2434 tree.read_with(cx, |tree, _| {
2435 let entry = tree.entry_for_path(rel_path("")).unwrap();
2436 assert!(entry.is_private);
2437 });
2438}
2439
2440#[gpui::test]
2441async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2442 init_test(cx);
2443
2444 let fs = FakeFs::new(executor);
2445 fs.insert_tree(
2446 path!("/root"),
2447 json!({
2448 ".git": {},
2449 "subproject": {
2450 "a.txt": "A"
2451 }
2452 }),
2453 )
2454 .await;
2455 let worktree = Worktree::local(
2456 path!("/root/subproject").as_ref(),
2457 true,
2458 fs.clone(),
2459 Arc::default(),
2460 true,
2461 WorktreeId::from_proto(0),
2462 &mut cx.to_async(),
2463 )
2464 .await
2465 .unwrap();
2466 worktree
2467 .update(cx, |worktree, _| {
2468 worktree.as_local().unwrap().scan_complete()
2469 })
2470 .await;
2471 cx.run_until_parked();
2472 let repos = worktree.update(cx, |worktree, _| {
2473 worktree.as_local().unwrap().repositories()
2474 });
2475 pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2476
2477 fs.touch_path(path!("/root/subproject")).await;
2478 worktree
2479 .update(cx, |worktree, _| {
2480 worktree.as_local().unwrap().scan_complete()
2481 })
2482 .await;
2483 cx.run_until_parked();
2484
2485 let repos = worktree.update(cx, |worktree, _| {
2486 worktree.as_local().unwrap().repositories()
2487 });
2488 pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2489}
2490
2491#[gpui::test]
2492async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2493 init_test(cx);
2494
2495 let home = paths::home_dir();
2496 let fs = FakeFs::new(executor);
2497 fs.insert_tree(
2498 home,
2499 json!({
2500 ".config": {
2501 "git": {
2502 "ignore": "foo\n/bar\nbaz\n"
2503 }
2504 },
2505 "project": {
2506 ".git": {},
2507 ".gitignore": "!baz",
2508 "foo": "",
2509 "bar": "",
2510 "sub": {
2511 "bar": "",
2512 },
2513 "subrepo": {
2514 ".git": {},
2515 "bar": ""
2516 },
2517 "baz": ""
2518 }
2519 }),
2520 )
2521 .await;
2522 let worktree = Worktree::local(
2523 home.join("project"),
2524 true,
2525 fs.clone(),
2526 Arc::default(),
2527 true,
2528 WorktreeId::from_proto(0),
2529 &mut cx.to_async(),
2530 )
2531 .await
2532 .unwrap();
2533 worktree
2534 .update(cx, |worktree, _| {
2535 worktree.as_local().unwrap().scan_complete()
2536 })
2537 .await;
2538 cx.run_until_parked();
2539
2540 // .gitignore overrides excludesFile, and anchored paths in excludesFile are resolved
2541 // relative to the nearest containing repository
2542 worktree.update(cx, |worktree, _cx| {
2543 check_worktree_entries(
2544 worktree,
2545 &[],
2546 &["foo", "bar", "subrepo/bar"],
2547 &["sub/bar", "baz"],
2548 &[],
2549 );
2550 });
2551
2552 // Ignore statuses are updated when excludesFile changes
2553 fs.write(
2554 &home.join(".config").join("git").join("ignore"),
2555 "/bar\nbaz\n".as_bytes(),
2556 )
2557 .await
2558 .unwrap();
2559 worktree
2560 .update(cx, |worktree, _| {
2561 worktree.as_local().unwrap().scan_complete()
2562 })
2563 .await;
2564 cx.run_until_parked();
2565
2566 worktree.update(cx, |worktree, _cx| {
2567 check_worktree_entries(
2568 worktree,
2569 &[],
2570 &["bar", "subrepo/bar"],
2571 &["foo", "sub/bar", "baz"],
2572 &[],
2573 );
2574 });
2575
2576 // Statuses are updated when .git added/removed
2577 fs.remove_dir(
2578 &home.join("project").join("subrepo").join(".git"),
2579 RemoveOptions {
2580 recursive: true,
2581 ..Default::default()
2582 },
2583 )
2584 .await
2585 .unwrap();
2586 worktree
2587 .update(cx, |worktree, _| {
2588 worktree.as_local().unwrap().scan_complete()
2589 })
2590 .await;
2591 cx.run_until_parked();
2592
2593 worktree.update(cx, |worktree, _cx| {
2594 check_worktree_entries(
2595 worktree,
2596 &[],
2597 &["bar"],
2598 &["foo", "sub/bar", "baz", "subrepo/bar"],
2599 &[],
2600 );
2601 });
2602}
2603
2604#[gpui::test]
2605async fn test_repo_exclude(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2606 init_test(cx);
2607
2608 let fs = FakeFs::new(executor);
2609 let project_dir = Path::new(path!("/project"));
2610 fs.insert_tree(
2611 project_dir,
2612 json!({
2613 ".git": {
2614 "info": {
2615 "exclude": ".env.*"
2616 }
2617 },
2618 ".env.example": "secret=xxxx",
2619 ".env.local": "secret=1234",
2620 ".gitignore": "!.env.example",
2621 "README.md": "# Repo Exclude",
2622 "src": {
2623 "main.rs": "fn main() {}",
2624 },
2625 }),
2626 )
2627 .await;
2628
2629 let worktree = Worktree::local(
2630 project_dir,
2631 true,
2632 fs.clone(),
2633 Default::default(),
2634 true,
2635 WorktreeId::from_proto(0),
2636 &mut cx.to_async(),
2637 )
2638 .await
2639 .unwrap();
2640 worktree
2641 .update(cx, |worktree, _| {
2642 worktree.as_local().unwrap().scan_complete()
2643 })
2644 .await;
2645 cx.run_until_parked();
2646
2647 // .gitignore overrides .git/info/exclude
2648 worktree.update(cx, |worktree, _cx| {
2649 let expected_excluded_paths = [];
2650 let expected_ignored_paths = [".env.local"];
2651 let expected_tracked_paths = [".env.example", "README.md", "src/main.rs"];
2652 let expected_included_paths = [];
2653
2654 check_worktree_entries(
2655 worktree,
2656 &expected_excluded_paths,
2657 &expected_ignored_paths,
2658 &expected_tracked_paths,
2659 &expected_included_paths,
2660 );
2661 });
2662
2663 // Ignore statuses are updated when .git/info/exclude file changes
2664 fs.write(
2665 &project_dir.join(DOT_GIT).join(REPO_EXCLUDE),
2666 ".env.example".as_bytes(),
2667 )
2668 .await
2669 .unwrap();
2670 worktree
2671 .update(cx, |worktree, _| {
2672 worktree.as_local().unwrap().scan_complete()
2673 })
2674 .await;
2675 cx.run_until_parked();
2676
2677 worktree.update(cx, |worktree, _cx| {
2678 let expected_excluded_paths = [];
2679 let expected_ignored_paths = [];
2680 let expected_tracked_paths = [".env.example", ".env.local", "README.md", "src/main.rs"];
2681 let expected_included_paths = [];
2682
2683 check_worktree_entries(
2684 worktree,
2685 &expected_excluded_paths,
2686 &expected_ignored_paths,
2687 &expected_tracked_paths,
2688 &expected_included_paths,
2689 );
2690 });
2691}
2692
2693#[track_caller]
2694fn check_worktree_entries(
2695 tree: &Worktree,
2696 expected_excluded_paths: &[&str],
2697 expected_ignored_paths: &[&str],
2698 expected_tracked_paths: &[&str],
2699 expected_included_paths: &[&str],
2700) {
2701 for path in expected_excluded_paths {
2702 let entry = tree.entry_for_path(rel_path(path));
2703 assert!(
2704 entry.is_none(),
2705 "expected path '{path}' to be excluded, but got entry: {entry:?}",
2706 );
2707 }
2708 for path in expected_ignored_paths {
2709 let entry = tree
2710 .entry_for_path(rel_path(path))
2711 .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
2712 assert!(
2713 entry.is_ignored,
2714 "expected path '{path}' to be ignored, but got entry: {entry:?}",
2715 );
2716 }
2717 for path in expected_tracked_paths {
2718 let entry = tree
2719 .entry_for_path(rel_path(path))
2720 .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
2721 assert!(
2722 !entry.is_ignored || entry.is_always_included,
2723 "expected path '{path}' to be tracked, but got entry: {entry:?}",
2724 );
2725 }
2726 for path in expected_included_paths {
2727 let entry = tree
2728 .entry_for_path(rel_path(path))
2729 .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
2730 assert!(
2731 entry.is_always_included,
2732 "expected path '{path}' to always be included, but got entry: {entry:?}",
2733 );
2734 }
2735}
2736
2737fn init_test(cx: &mut gpui::TestAppContext) {
2738 zlog::init_test();
2739
2740 cx.update(|cx| {
2741 let settings_store = SettingsStore::test(cx);
2742 cx.set_global(settings_store);
2743 });
2744}
2745
2746#[gpui::test]
2747async fn test_load_file_encoding(cx: &mut TestAppContext) {
2748 init_test(cx);
2749
2750 struct TestCase {
2751 name: &'static str,
2752 bytes: Vec<u8>,
2753 expected_text: &'static str,
2754 }
2755
2756 // --- Success Cases ---
2757 let success_cases = vec![
2758 TestCase {
2759 name: "utf8.txt",
2760 bytes: "ใใใซใกใฏ".as_bytes().to_vec(),
2761 expected_text: "ใใใซใกใฏ",
2762 },
2763 TestCase {
2764 name: "sjis.txt",
2765 bytes: vec![0x82, 0xb1, 0x82, 0xf1, 0x82, 0xc9, 0x82, 0xbf, 0x82, 0xcd],
2766 expected_text: "ใใใซใกใฏ",
2767 },
2768 TestCase {
2769 name: "eucjp.txt",
2770 bytes: vec![0xa4, 0xb3, 0xa4, 0xf3, 0xa4, 0xcb, 0xa4, 0xc1, 0xa4, 0xcf],
2771 expected_text: "ใใใซใกใฏ",
2772 },
2773 TestCase {
2774 name: "iso2022jp.txt",
2775 bytes: vec![
2776 0x1b, 0x24, 0x42, 0x24, 0x33, 0x24, 0x73, 0x24, 0x4b, 0x24, 0x41, 0x24, 0x4f, 0x1b,
2777 0x28, 0x42,
2778 ],
2779 expected_text: "ใใใซใกใฏ",
2780 },
2781 TestCase {
2782 name: "win1252.txt",
2783 bytes: vec![0x43, 0x61, 0x66, 0xe9],
2784 expected_text: "Cafรฉ",
2785 },
2786 TestCase {
2787 name: "gbk.txt",
2788 bytes: vec![
2789 0xbd, 0xf1, 0xcc, 0xec, 0xcc, 0xec, 0xc6, 0xf8, 0xb2, 0xbb, 0xb4, 0xed,
2790 ],
2791 expected_text: "ไปๅคฉๅคฉๆฐไธ้",
2792 },
2793 // UTF-16LE with BOM
2794 TestCase {
2795 name: "utf16le_bom.txt",
2796 bytes: vec![
2797 0xFF, 0xFE, // BOM
2798 0x53, 0x30, 0x93, 0x30, 0x6B, 0x30, 0x61, 0x30, 0x6F, 0x30,
2799 ],
2800 expected_text: "ใใใซใกใฏ",
2801 },
2802 // UTF-16BE with BOM
2803 TestCase {
2804 name: "utf16be_bom.txt",
2805 bytes: vec![
2806 0xFE, 0xFF, // BOM
2807 0x30, 0x53, 0x30, 0x93, 0x30, 0x6B, 0x30, 0x61, 0x30, 0x6F,
2808 ],
2809 expected_text: "ใใใซใกใฏ",
2810 },
2811 // UTF-16LE without BOM (ASCII only)
2812 // This relies on the "null byte heuristic" we implemented.
2813 // "ABC" -> 41 00 42 00 43 00
2814 TestCase {
2815 name: "utf16le_ascii_no_bom.txt",
2816 bytes: vec![0x41, 0x00, 0x42, 0x00, 0x43, 0x00],
2817 expected_text: "ABC",
2818 },
2819 ];
2820
2821 // --- Failure Cases ---
2822 let failure_cases = vec![
2823 // Binary File (Should be detected by heuristic and return Error)
2824 // Contains random bytes and mixed nulls that don't match UTF-16 patterns
2825 TestCase {
2826 name: "binary.bin",
2827 bytes: vec![0x00, 0xFF, 0x12, 0x00, 0x99, 0x88, 0x77, 0x66, 0x00],
2828 expected_text: "", // Not used
2829 },
2830 ];
2831
2832 let root_path = if cfg!(windows) {
2833 Path::new("C:\\root")
2834 } else {
2835 Path::new("/root")
2836 };
2837
2838 let fs = FakeFs::new(cx.background_executor.clone());
2839 fs.create_dir(root_path).await.unwrap();
2840
2841 for case in success_cases.iter().chain(failure_cases.iter()) {
2842 let path = root_path.join(case.name);
2843 fs.write(&path, &case.bytes).await.unwrap();
2844 }
2845
2846 let tree = Worktree::local(
2847 root_path,
2848 true,
2849 fs,
2850 Default::default(),
2851 true,
2852 WorktreeId::from_proto(0),
2853 &mut cx.to_async(),
2854 )
2855 .await
2856 .unwrap();
2857
2858 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2859 .await;
2860
2861 let rel_path = |name: &str| {
2862 RelPath::new(&Path::new(name), PathStyle::local())
2863 .unwrap()
2864 .into_arc()
2865 };
2866
2867 // Run Success Tests
2868 for case in success_cases {
2869 let loaded = tree
2870 .update(cx, |tree, cx| tree.load_file(&rel_path(case.name), cx))
2871 .await;
2872 if let Err(e) = &loaded {
2873 panic!("Failed to load success case '{}': {:?}", case.name, e);
2874 }
2875 let loaded = loaded.unwrap();
2876 assert_eq!(
2877 loaded.text, case.expected_text,
2878 "Encoding mismatch for file: {}",
2879 case.name
2880 );
2881 }
2882
2883 // Run Failure Tests
2884 for case in failure_cases {
2885 let loaded = tree
2886 .update(cx, |tree, cx| tree.load_file(&rel_path(case.name), cx))
2887 .await;
2888 assert!(
2889 loaded.is_err(),
2890 "Failure case '{}' unexpectedly succeeded! It should have been detected as binary.",
2891 case.name
2892 );
2893 let err_msg = loaded.unwrap_err().to_string();
2894 println!("Got expected error for {}: {}", case.name, err_msg);
2895 }
2896}
2897
2898#[gpui::test]
2899async fn test_write_file_encoding(cx: &mut gpui::TestAppContext) {
2900 init_test(cx);
2901 let fs = FakeFs::new(cx.executor());
2902
2903 let root_path = if cfg!(windows) {
2904 Path::new("C:\\root")
2905 } else {
2906 Path::new("/root")
2907 };
2908 fs.create_dir(root_path).await.unwrap();
2909
2910 let worktree = Worktree::local(
2911 root_path,
2912 true,
2913 fs.clone(),
2914 Default::default(),
2915 true,
2916 WorktreeId::from_proto(0),
2917 &mut cx.to_async(),
2918 )
2919 .await
2920 .unwrap();
2921
2922 // Define test case structure
2923 struct TestCase {
2924 name: &'static str,
2925 text: &'static str,
2926 encoding: &'static encoding_rs::Encoding,
2927 has_bom: bool,
2928 expected_bytes: Vec<u8>,
2929 }
2930
2931 let cases = vec![
2932 // Shift_JIS with Japanese
2933 TestCase {
2934 name: "Shift_JIS with Japanese",
2935 text: "ใใใซใกใฏ",
2936 encoding: encoding_rs::SHIFT_JIS,
2937 has_bom: false,
2938 expected_bytes: vec![0x82, 0xb1, 0x82, 0xf1, 0x82, 0xc9, 0x82, 0xbf, 0x82, 0xcd],
2939 },
2940 // UTF-8 No BOM
2941 TestCase {
2942 name: "UTF-8 No BOM",
2943 text: "AB",
2944 encoding: encoding_rs::UTF_8,
2945 has_bom: false,
2946 expected_bytes: vec![0x41, 0x42],
2947 },
2948 // UTF-8 with BOM
2949 TestCase {
2950 name: "UTF-8 with BOM",
2951 text: "AB",
2952 encoding: encoding_rs::UTF_8,
2953 has_bom: true,
2954 expected_bytes: vec![0xEF, 0xBB, 0xBF, 0x41, 0x42],
2955 },
2956 // UTF-16LE No BOM with Japanese
2957 // NOTE: This passes thanks to the manual encoding fix implemented in `write_file`.
2958 TestCase {
2959 name: "UTF-16LE No BOM with Japanese",
2960 text: "ใใใซใกใฏ",
2961 encoding: encoding_rs::UTF_16LE,
2962 has_bom: false,
2963 expected_bytes: vec![0x53, 0x30, 0x93, 0x30, 0x6b, 0x30, 0x61, 0x30, 0x6f, 0x30],
2964 },
2965 // UTF-16LE with BOM
2966 TestCase {
2967 name: "UTF-16LE with BOM",
2968 text: "A",
2969 encoding: encoding_rs::UTF_16LE,
2970 has_bom: true,
2971 expected_bytes: vec![0xFF, 0xFE, 0x41, 0x00],
2972 },
2973 // UTF-16BE No BOM with Japanese
2974 // NOTE: This passes thanks to the manual encoding fix.
2975 TestCase {
2976 name: "UTF-16BE No BOM with Japanese",
2977 text: "ใใใซใกใฏ",
2978 encoding: encoding_rs::UTF_16BE,
2979 has_bom: false,
2980 expected_bytes: vec![0x30, 0x53, 0x30, 0x93, 0x30, 0x6b, 0x30, 0x61, 0x30, 0x6f],
2981 },
2982 // UTF-16BE with BOM
2983 TestCase {
2984 name: "UTF-16BE with BOM",
2985 text: "A",
2986 encoding: encoding_rs::UTF_16BE,
2987 has_bom: true,
2988 expected_bytes: vec![0xFE, 0xFF, 0x00, 0x41],
2989 },
2990 ];
2991
2992 for (i, case) in cases.into_iter().enumerate() {
2993 let file_name = format!("test_{}.txt", i);
2994 let path: Arc<Path> = Path::new(&file_name).into();
2995 let file_path = root_path.join(&file_name);
2996
2997 fs.insert_file(&file_path, "".into()).await;
2998
2999 let rel_path = RelPath::new(&path, PathStyle::local()).unwrap().into_arc();
3000 let text = text::Rope::from(case.text);
3001
3002 let task = worktree.update(cx, |wt, cx| {
3003 wt.write_file(
3004 rel_path,
3005 text,
3006 text::LineEnding::Unix,
3007 case.encoding,
3008 case.has_bom,
3009 cx,
3010 )
3011 });
3012
3013 if let Err(e) = task.await {
3014 panic!("Unexpected error in case '{}': {:?}", case.name, e);
3015 }
3016
3017 let bytes = fs.load_bytes(&file_path).await.unwrap();
3018
3019 assert_eq!(
3020 bytes, case.expected_bytes,
3021 "case '{}' mismatch. Expected {:?}, but got {:?}",
3022 case.name, case.expected_bytes, bytes
3023 );
3024 }
3025}
3026
3027#[gpui::test]
3028async fn test_refresh_entries_for_paths_creates_ancestors(cx: &mut TestAppContext) {
3029 init_test(cx);
3030 let fs = FakeFs::new(cx.background_executor.clone());
3031 fs.insert_tree(
3032 "/root",
3033 json!({
3034 "a": {
3035 "b": {
3036 "c": {
3037 "deep_file.txt": "content",
3038 "sibling.txt": "content"
3039 },
3040 "d": {
3041 "under_sibling_dir.txt": "content"
3042 }
3043 }
3044 }
3045 }),
3046 )
3047 .await;
3048
3049 let tree = Worktree::local(
3050 Path::new("/root"),
3051 true,
3052 fs.clone(),
3053 Default::default(),
3054 false, // Disable scanning so the initial scan doesn't discover any entries
3055 WorktreeId::from_proto(0),
3056 &mut cx.to_async(),
3057 )
3058 .await
3059 .unwrap();
3060
3061 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
3062 .await;
3063
3064 tree.read_with(cx, |tree, _| {
3065 assert_eq!(
3066 tree.entries(true, 0)
3067 .map(|e| e.path.as_ref())
3068 .collect::<Vec<_>>(),
3069 &[rel_path("")],
3070 "Only root entry should exist when scanning is disabled"
3071 );
3072
3073 assert!(tree.entry_for_path(rel_path("a")).is_none());
3074 assert!(tree.entry_for_path(rel_path("a/b")).is_none());
3075 assert!(tree.entry_for_path(rel_path("a/b/c")).is_none());
3076 assert!(
3077 tree.entry_for_path(rel_path("a/b/c/deep_file.txt"))
3078 .is_none()
3079 );
3080 });
3081
3082 tree.read_with(cx, |tree, _| {
3083 tree.as_local()
3084 .unwrap()
3085 .refresh_entries_for_paths(vec![rel_path("a/b/c/deep_file.txt").into()])
3086 })
3087 .recv()
3088 .await;
3089
3090 tree.read_with(cx, |tree, _| {
3091 assert_eq!(
3092 tree.entries(true, 0)
3093 .map(|e| e.path.as_ref())
3094 .collect::<Vec<_>>(),
3095 &[
3096 rel_path(""),
3097 rel_path("a"),
3098 rel_path("a/b"),
3099 rel_path("a/b/c"),
3100 rel_path("a/b/c/deep_file.txt"),
3101 rel_path("a/b/c/sibling.txt"),
3102 rel_path("a/b/d"),
3103 ],
3104 "All ancestors should be created when refreshing a deeply nested path"
3105 );
3106 });
3107}