1use super::*;
2use collections::HashSet;
3use gpui::{Empty, Entity, TestAppContext, VisualTestContext, WindowHandle};
4use pretty_assertions::assert_eq;
5use project::{FakeFs, WorktreeSettings};
6use serde_json::json;
7use settings::SettingsStore;
8use std::path::{Path, PathBuf};
9use util::{path, separator};
10use workspace::{
11 AppState, Pane,
12 item::{Item, ProjectItem},
13 register_project_item,
14};
15
16#[gpui::test]
17async fn test_visible_list(cx: &mut gpui::TestAppContext) {
18 init_test(cx);
19
20 let fs = FakeFs::new(cx.executor().clone());
21 fs.insert_tree(
22 "/root1",
23 json!({
24 ".dockerignore": "",
25 ".git": {
26 "HEAD": "",
27 },
28 "a": {
29 "0": { "q": "", "r": "", "s": "" },
30 "1": { "t": "", "u": "" },
31 "2": { "v": "", "w": "", "x": "", "y": "" },
32 },
33 "b": {
34 "3": { "Q": "" },
35 "4": { "R": "", "S": "", "T": "", "U": "" },
36 },
37 "C": {
38 "5": {},
39 "6": { "V": "", "W": "" },
40 "7": { "X": "" },
41 "8": { "Y": {}, "Z": "" }
42 }
43 }),
44 )
45 .await;
46 fs.insert_tree(
47 "/root2",
48 json!({
49 "d": {
50 "9": ""
51 },
52 "e": {}
53 }),
54 )
55 .await;
56
57 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
58 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
59 let cx = &mut VisualTestContext::from_window(*workspace, cx);
60 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
61 assert_eq!(
62 visible_entries_as_strings(&panel, 0..50, cx),
63 &[
64 "v root1",
65 " > .git",
66 " > a",
67 " > b",
68 " > C",
69 " .dockerignore",
70 "v root2",
71 " > d",
72 " > e",
73 ]
74 );
75
76 toggle_expand_dir(&panel, "root1/b", cx);
77 assert_eq!(
78 visible_entries_as_strings(&panel, 0..50, cx),
79 &[
80 "v root1",
81 " > .git",
82 " > a",
83 " v b <== selected",
84 " > 3",
85 " > 4",
86 " > C",
87 " .dockerignore",
88 "v root2",
89 " > d",
90 " > e",
91 ]
92 );
93
94 assert_eq!(
95 visible_entries_as_strings(&panel, 6..9, cx),
96 &[
97 //
98 " > C",
99 " .dockerignore",
100 "v root2",
101 ]
102 );
103}
104
105#[gpui::test]
106async fn test_opening_file(cx: &mut gpui::TestAppContext) {
107 init_test_with_editor(cx);
108
109 let fs = FakeFs::new(cx.executor().clone());
110 fs.insert_tree(
111 path!("/src"),
112 json!({
113 "test": {
114 "first.rs": "// First Rust file",
115 "second.rs": "// Second Rust file",
116 "third.rs": "// Third Rust file",
117 }
118 }),
119 )
120 .await;
121
122 let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await;
123 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
124 let cx = &mut VisualTestContext::from_window(*workspace, cx);
125 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
126
127 toggle_expand_dir(&panel, "src/test", cx);
128 select_path(&panel, "src/test/first.rs", cx);
129 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
130 cx.executor().run_until_parked();
131 assert_eq!(
132 visible_entries_as_strings(&panel, 0..10, cx),
133 &[
134 "v src",
135 " v test",
136 " first.rs <== selected <== marked",
137 " second.rs",
138 " third.rs"
139 ]
140 );
141 ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
142
143 select_path(&panel, "src/test/second.rs", cx);
144 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
145 cx.executor().run_until_parked();
146 assert_eq!(
147 visible_entries_as_strings(&panel, 0..10, cx),
148 &[
149 "v src",
150 " v test",
151 " first.rs",
152 " second.rs <== selected <== marked",
153 " third.rs"
154 ]
155 );
156 ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
157}
158
159#[gpui::test]
160async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
161 init_test(cx);
162 cx.update(|cx| {
163 cx.update_global::<SettingsStore, _>(|store, cx| {
164 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
165 worktree_settings.file_scan_exclusions =
166 Some(vec!["**/.git".to_string(), "**/4/**".to_string()]);
167 });
168 });
169 });
170
171 let fs = FakeFs::new(cx.background_executor.clone());
172 fs.insert_tree(
173 "/root1",
174 json!({
175 ".dockerignore": "",
176 ".git": {
177 "HEAD": "",
178 },
179 "a": {
180 "0": { "q": "", "r": "", "s": "" },
181 "1": { "t": "", "u": "" },
182 "2": { "v": "", "w": "", "x": "", "y": "" },
183 },
184 "b": {
185 "3": { "Q": "" },
186 "4": { "R": "", "S": "", "T": "", "U": "" },
187 },
188 "C": {
189 "5": {},
190 "6": { "V": "", "W": "" },
191 "7": { "X": "" },
192 "8": { "Y": {}, "Z": "" }
193 }
194 }),
195 )
196 .await;
197 fs.insert_tree(
198 "/root2",
199 json!({
200 "d": {
201 "4": ""
202 },
203 "e": {}
204 }),
205 )
206 .await;
207
208 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
209 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
210 let cx = &mut VisualTestContext::from_window(*workspace, cx);
211 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
212 assert_eq!(
213 visible_entries_as_strings(&panel, 0..50, cx),
214 &[
215 "v root1",
216 " > a",
217 " > b",
218 " > C",
219 " .dockerignore",
220 "v root2",
221 " > d",
222 " > e",
223 ]
224 );
225
226 toggle_expand_dir(&panel, "root1/b", cx);
227 assert_eq!(
228 visible_entries_as_strings(&panel, 0..50, cx),
229 &[
230 "v root1",
231 " > a",
232 " v b <== selected",
233 " > 3",
234 " > C",
235 " .dockerignore",
236 "v root2",
237 " > d",
238 " > e",
239 ]
240 );
241
242 toggle_expand_dir(&panel, "root2/d", cx);
243 assert_eq!(
244 visible_entries_as_strings(&panel, 0..50, cx),
245 &[
246 "v root1",
247 " > a",
248 " v b",
249 " > 3",
250 " > C",
251 " .dockerignore",
252 "v root2",
253 " v d <== selected",
254 " > e",
255 ]
256 );
257
258 toggle_expand_dir(&panel, "root2/e", cx);
259 assert_eq!(
260 visible_entries_as_strings(&panel, 0..50, cx),
261 &[
262 "v root1",
263 " > a",
264 " v b",
265 " > 3",
266 " > C",
267 " .dockerignore",
268 "v root2",
269 " v d",
270 " v e <== selected",
271 ]
272 );
273}
274
275#[gpui::test]
276async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) {
277 init_test(cx);
278
279 let fs = FakeFs::new(cx.executor().clone());
280 fs.insert_tree(
281 path!("/root1"),
282 json!({
283 "dir_1": {
284 "nested_dir_1": {
285 "nested_dir_2": {
286 "nested_dir_3": {
287 "file_a.java": "// File contents",
288 "file_b.java": "// File contents",
289 "file_c.java": "// File contents",
290 "nested_dir_4": {
291 "nested_dir_5": {
292 "file_d.java": "// File contents",
293 }
294 }
295 }
296 }
297 }
298 }
299 }),
300 )
301 .await;
302 fs.insert_tree(
303 path!("/root2"),
304 json!({
305 "dir_2": {
306 "file_1.java": "// File contents",
307 }
308 }),
309 )
310 .await;
311
312 let project = Project::test(
313 fs.clone(),
314 [path!("/root1").as_ref(), path!("/root2").as_ref()],
315 cx,
316 )
317 .await;
318 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
319 let cx = &mut VisualTestContext::from_window(*workspace, cx);
320 cx.update(|_, cx| {
321 let settings = *ProjectPanelSettings::get_global(cx);
322 ProjectPanelSettings::override_global(
323 ProjectPanelSettings {
324 auto_fold_dirs: true,
325 ..settings
326 },
327 cx,
328 );
329 });
330 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
331 assert_eq!(
332 visible_entries_as_strings(&panel, 0..10, cx),
333 &[
334 separator!("v root1"),
335 separator!(" > dir_1/nested_dir_1/nested_dir_2/nested_dir_3"),
336 separator!("v root2"),
337 separator!(" > dir_2"),
338 ]
339 );
340
341 toggle_expand_dir(
342 &panel,
343 "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
344 cx,
345 );
346 assert_eq!(
347 visible_entries_as_strings(&panel, 0..10, cx),
348 &[
349 separator!("v root1"),
350 separator!(" v dir_1/nested_dir_1/nested_dir_2/nested_dir_3 <== selected"),
351 separator!(" > nested_dir_4/nested_dir_5"),
352 separator!(" file_a.java"),
353 separator!(" file_b.java"),
354 separator!(" file_c.java"),
355 separator!("v root2"),
356 separator!(" > dir_2"),
357 ]
358 );
359
360 toggle_expand_dir(
361 &panel,
362 "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
363 cx,
364 );
365 assert_eq!(
366 visible_entries_as_strings(&panel, 0..10, cx),
367 &[
368 separator!("v root1"),
369 separator!(" v dir_1/nested_dir_1/nested_dir_2/nested_dir_3"),
370 separator!(" v nested_dir_4/nested_dir_5 <== selected"),
371 separator!(" file_d.java"),
372 separator!(" file_a.java"),
373 separator!(" file_b.java"),
374 separator!(" file_c.java"),
375 separator!("v root2"),
376 separator!(" > dir_2"),
377 ]
378 );
379 toggle_expand_dir(&panel, "root2/dir_2", cx);
380 assert_eq!(
381 visible_entries_as_strings(&panel, 0..10, cx),
382 &[
383 separator!("v root1"),
384 separator!(" v dir_1/nested_dir_1/nested_dir_2/nested_dir_3"),
385 separator!(" v nested_dir_4/nested_dir_5"),
386 separator!(" file_d.java"),
387 separator!(" file_a.java"),
388 separator!(" file_b.java"),
389 separator!(" file_c.java"),
390 separator!("v root2"),
391 separator!(" v dir_2 <== selected"),
392 separator!(" file_1.java"),
393 ]
394 );
395}
396
397#[gpui::test(iterations = 30)]
398async fn test_editing_files(cx: &mut gpui::TestAppContext) {
399 init_test(cx);
400
401 let fs = FakeFs::new(cx.executor().clone());
402 fs.insert_tree(
403 "/root1",
404 json!({
405 ".dockerignore": "",
406 ".git": {
407 "HEAD": "",
408 },
409 "a": {
410 "0": { "q": "", "r": "", "s": "" },
411 "1": { "t": "", "u": "" },
412 "2": { "v": "", "w": "", "x": "", "y": "" },
413 },
414 "b": {
415 "3": { "Q": "" },
416 "4": { "R": "", "S": "", "T": "", "U": "" },
417 },
418 "C": {
419 "5": {},
420 "6": { "V": "", "W": "" },
421 "7": { "X": "" },
422 "8": { "Y": {}, "Z": "" }
423 }
424 }),
425 )
426 .await;
427 fs.insert_tree(
428 "/root2",
429 json!({
430 "d": {
431 "9": ""
432 },
433 "e": {}
434 }),
435 )
436 .await;
437
438 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
439 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
440 let cx = &mut VisualTestContext::from_window(*workspace, cx);
441 let panel = workspace
442 .update(cx, |workspace, window, cx| {
443 let panel = ProjectPanel::new(workspace, window, cx);
444 workspace.add_panel(panel.clone(), window, cx);
445 panel
446 })
447 .unwrap();
448
449 select_path(&panel, "root1", cx);
450 assert_eq!(
451 visible_entries_as_strings(&panel, 0..10, cx),
452 &[
453 "v root1 <== selected",
454 " > .git",
455 " > a",
456 " > b",
457 " > C",
458 " .dockerignore",
459 "v root2",
460 " > d",
461 " > e",
462 ]
463 );
464
465 // Add a file with the root folder selected. The filename editor is placed
466 // before the first file in the root folder.
467 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
468 panel.update_in(cx, |panel, window, cx| {
469 assert!(panel.filename_editor.read(cx).is_focused(window));
470 });
471 assert_eq!(
472 visible_entries_as_strings(&panel, 0..10, cx),
473 &[
474 "v root1",
475 " > .git",
476 " > a",
477 " > b",
478 " > C",
479 " [EDITOR: ''] <== selected",
480 " .dockerignore",
481 "v root2",
482 " > d",
483 " > e",
484 ]
485 );
486
487 let confirm = panel.update_in(cx, |panel, window, cx| {
488 panel.filename_editor.update(cx, |editor, cx| {
489 editor.set_text("the-new-filename", window, cx)
490 });
491 panel.confirm_edit(window, cx).unwrap()
492 });
493 assert_eq!(
494 visible_entries_as_strings(&panel, 0..10, cx),
495 &[
496 "v root1",
497 " > .git",
498 " > a",
499 " > b",
500 " > C",
501 " [PROCESSING: 'the-new-filename'] <== selected",
502 " .dockerignore",
503 "v root2",
504 " > d",
505 " > e",
506 ]
507 );
508
509 confirm.await.unwrap();
510 assert_eq!(
511 visible_entries_as_strings(&panel, 0..10, cx),
512 &[
513 "v root1",
514 " > .git",
515 " > a",
516 " > b",
517 " > C",
518 " .dockerignore",
519 " the-new-filename <== selected <== marked",
520 "v root2",
521 " > d",
522 " > e",
523 ]
524 );
525
526 select_path(&panel, "root1/b", cx);
527 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
528 assert_eq!(
529 visible_entries_as_strings(&panel, 0..10, cx),
530 &[
531 "v root1",
532 " > .git",
533 " > a",
534 " v b",
535 " > 3",
536 " > 4",
537 " [EDITOR: ''] <== selected",
538 " > C",
539 " .dockerignore",
540 " the-new-filename",
541 ]
542 );
543
544 panel
545 .update_in(cx, |panel, window, cx| {
546 panel.filename_editor.update(cx, |editor, cx| {
547 editor.set_text("another-filename.txt", window, cx)
548 });
549 panel.confirm_edit(window, cx).unwrap()
550 })
551 .await
552 .unwrap();
553 assert_eq!(
554 visible_entries_as_strings(&panel, 0..10, cx),
555 &[
556 "v root1",
557 " > .git",
558 " > a",
559 " v b",
560 " > 3",
561 " > 4",
562 " another-filename.txt <== selected <== marked",
563 " > C",
564 " .dockerignore",
565 " the-new-filename",
566 ]
567 );
568
569 select_path(&panel, "root1/b/another-filename.txt", cx);
570 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
571 assert_eq!(
572 visible_entries_as_strings(&panel, 0..10, cx),
573 &[
574 "v root1",
575 " > .git",
576 " > a",
577 " v b",
578 " > 3",
579 " > 4",
580 " [EDITOR: 'another-filename.txt'] <== selected <== marked",
581 " > C",
582 " .dockerignore",
583 " the-new-filename",
584 ]
585 );
586
587 let confirm = panel.update_in(cx, |panel, window, cx| {
588 panel.filename_editor.update(cx, |editor, cx| {
589 let file_name_selections = editor.selections.all::<usize>(cx);
590 assert_eq!(
591 file_name_selections.len(),
592 1,
593 "File editing should have a single selection, but got: {file_name_selections:?}"
594 );
595 let file_name_selection = &file_name_selections[0];
596 assert_eq!(
597 file_name_selection.start, 0,
598 "Should select the file name from the start"
599 );
600 assert_eq!(
601 file_name_selection.end,
602 "another-filename".len(),
603 "Should not select file extension"
604 );
605
606 editor.set_text("a-different-filename.tar.gz", window, cx)
607 });
608 panel.confirm_edit(window, cx).unwrap()
609 });
610 assert_eq!(
611 visible_entries_as_strings(&panel, 0..10, cx),
612 &[
613 "v root1",
614 " > .git",
615 " > a",
616 " v b",
617 " > 3",
618 " > 4",
619 " [PROCESSING: 'a-different-filename.tar.gz'] <== selected <== marked",
620 " > C",
621 " .dockerignore",
622 " the-new-filename",
623 ]
624 );
625
626 confirm.await.unwrap();
627 assert_eq!(
628 visible_entries_as_strings(&panel, 0..10, cx),
629 &[
630 "v root1",
631 " > .git",
632 " > a",
633 " v b",
634 " > 3",
635 " > 4",
636 " a-different-filename.tar.gz <== selected",
637 " > C",
638 " .dockerignore",
639 " the-new-filename",
640 ]
641 );
642
643 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
644 assert_eq!(
645 visible_entries_as_strings(&panel, 0..10, cx),
646 &[
647 "v root1",
648 " > .git",
649 " > a",
650 " v b",
651 " > 3",
652 " > 4",
653 " [EDITOR: 'a-different-filename.tar.gz'] <== selected",
654 " > C",
655 " .dockerignore",
656 " the-new-filename",
657 ]
658 );
659
660 panel.update_in(cx, |panel, window, cx| {
661 panel.filename_editor.update(cx, |editor, cx| {
662 let file_name_selections = editor.selections.all::<usize>(cx);
663 assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
664 let file_name_selection = &file_name_selections[0];
665 assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
666 assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot..");
667
668 });
669 panel.cancel(&menu::Cancel, window, cx)
670 });
671
672 panel.update_in(cx, |panel, window, cx| {
673 panel.new_directory(&NewDirectory, window, cx)
674 });
675 assert_eq!(
676 visible_entries_as_strings(&panel, 0..10, cx),
677 &[
678 "v root1",
679 " > .git",
680 " > a",
681 " v b",
682 " > 3",
683 " > 4",
684 " > [EDITOR: ''] <== selected",
685 " a-different-filename.tar.gz",
686 " > C",
687 " .dockerignore",
688 ]
689 );
690
691 let confirm = panel.update_in(cx, |panel, window, cx| {
692 panel
693 .filename_editor
694 .update(cx, |editor, cx| editor.set_text("new-dir", window, cx));
695 panel.confirm_edit(window, cx).unwrap()
696 });
697 panel.update_in(cx, |panel, window, cx| {
698 panel.select_next(&Default::default(), window, cx)
699 });
700 assert_eq!(
701 visible_entries_as_strings(&panel, 0..10, cx),
702 &[
703 "v root1",
704 " > .git",
705 " > a",
706 " v b",
707 " > 3",
708 " > 4",
709 " > [PROCESSING: 'new-dir']",
710 " a-different-filename.tar.gz <== selected",
711 " > C",
712 " .dockerignore",
713 ]
714 );
715
716 confirm.await.unwrap();
717 assert_eq!(
718 visible_entries_as_strings(&panel, 0..10, cx),
719 &[
720 "v root1",
721 " > .git",
722 " > a",
723 " v b",
724 " > 3",
725 " > 4",
726 " > new-dir",
727 " a-different-filename.tar.gz <== selected",
728 " > C",
729 " .dockerignore",
730 ]
731 );
732
733 panel.update_in(cx, |panel, window, cx| {
734 panel.rename(&Default::default(), window, cx)
735 });
736 assert_eq!(
737 visible_entries_as_strings(&panel, 0..10, cx),
738 &[
739 "v root1",
740 " > .git",
741 " > a",
742 " v b",
743 " > 3",
744 " > 4",
745 " > new-dir",
746 " [EDITOR: 'a-different-filename.tar.gz'] <== selected",
747 " > C",
748 " .dockerignore",
749 ]
750 );
751
752 // Dismiss the rename editor when it loses focus.
753 workspace.update(cx, |_, window, _| window.blur()).unwrap();
754 assert_eq!(
755 visible_entries_as_strings(&panel, 0..10, cx),
756 &[
757 "v root1",
758 " > .git",
759 " > a",
760 " v b",
761 " > 3",
762 " > 4",
763 " > new-dir",
764 " a-different-filename.tar.gz <== selected",
765 " > C",
766 " .dockerignore",
767 ]
768 );
769
770 // Test empty filename and filename with only whitespace
771 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
772 assert_eq!(
773 visible_entries_as_strings(&panel, 0..10, cx),
774 &[
775 "v root1",
776 " > .git",
777 " > a",
778 " v b",
779 " > 3",
780 " > 4",
781 " > new-dir",
782 " [EDITOR: ''] <== selected",
783 " a-different-filename.tar.gz",
784 " > C",
785 ]
786 );
787 panel.update_in(cx, |panel, window, cx| {
788 panel.filename_editor.update(cx, |editor, cx| {
789 editor.set_text("", window, cx);
790 });
791 assert!(panel.confirm_edit(window, cx).is_none());
792 panel.filename_editor.update(cx, |editor, cx| {
793 editor.set_text(" ", window, cx);
794 });
795 assert!(panel.confirm_edit(window, cx).is_none());
796 panel.cancel(&menu::Cancel, window, cx)
797 });
798 assert_eq!(
799 visible_entries_as_strings(&panel, 0..10, cx),
800 &[
801 "v root1",
802 " > .git",
803 " > a",
804 " v b",
805 " > 3",
806 " > 4",
807 " > new-dir",
808 " a-different-filename.tar.gz <== selected",
809 " > C",
810 " .dockerignore",
811 ]
812 );
813}
814
815#[gpui::test(iterations = 10)]
816async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
817 init_test(cx);
818
819 let fs = FakeFs::new(cx.executor().clone());
820 fs.insert_tree(
821 "/root1",
822 json!({
823 ".dockerignore": "",
824 ".git": {
825 "HEAD": "",
826 },
827 "a": {
828 "0": { "q": "", "r": "", "s": "" },
829 "1": { "t": "", "u": "" },
830 "2": { "v": "", "w": "", "x": "", "y": "" },
831 },
832 "b": {
833 "3": { "Q": "" },
834 "4": { "R": "", "S": "", "T": "", "U": "" },
835 },
836 "C": {
837 "5": {},
838 "6": { "V": "", "W": "" },
839 "7": { "X": "" },
840 "8": { "Y": {}, "Z": "" }
841 }
842 }),
843 )
844 .await;
845 fs.insert_tree(
846 "/root2",
847 json!({
848 "d": {
849 "9": ""
850 },
851 "e": {}
852 }),
853 )
854 .await;
855
856 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
857 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
858 let cx = &mut VisualTestContext::from_window(*workspace, cx);
859 let panel = workspace
860 .update(cx, |workspace, window, cx| {
861 let panel = ProjectPanel::new(workspace, window, cx);
862 workspace.add_panel(panel.clone(), window, cx);
863 panel
864 })
865 .unwrap();
866
867 select_path(&panel, "root1", cx);
868 assert_eq!(
869 visible_entries_as_strings(&panel, 0..10, cx),
870 &[
871 "v root1 <== selected",
872 " > .git",
873 " > a",
874 " > b",
875 " > C",
876 " .dockerignore",
877 "v root2",
878 " > d",
879 " > e",
880 ]
881 );
882
883 // Add a file with the root folder selected. The filename editor is placed
884 // before the first file in the root folder.
885 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
886 panel.update_in(cx, |panel, window, cx| {
887 assert!(panel.filename_editor.read(cx).is_focused(window));
888 });
889 assert_eq!(
890 visible_entries_as_strings(&panel, 0..10, cx),
891 &[
892 "v root1",
893 " > .git",
894 " > a",
895 " > b",
896 " > C",
897 " [EDITOR: ''] <== selected",
898 " .dockerignore",
899 "v root2",
900 " > d",
901 " > e",
902 ]
903 );
904
905 let confirm = panel.update_in(cx, |panel, window, cx| {
906 panel.filename_editor.update(cx, |editor, cx| {
907 editor.set_text("/bdir1/dir2/the-new-filename", window, cx)
908 });
909 panel.confirm_edit(window, cx).unwrap()
910 });
911
912 assert_eq!(
913 visible_entries_as_strings(&panel, 0..10, cx),
914 &[
915 "v root1",
916 " > .git",
917 " > a",
918 " > b",
919 " > C",
920 " [PROCESSING: '/bdir1/dir2/the-new-filename'] <== selected",
921 " .dockerignore",
922 "v root2",
923 " > d",
924 " > e",
925 ]
926 );
927
928 confirm.await.unwrap();
929 assert_eq!(
930 visible_entries_as_strings(&panel, 0..13, cx),
931 &[
932 "v root1",
933 " > .git",
934 " > a",
935 " > b",
936 " v bdir1",
937 " v dir2",
938 " the-new-filename <== selected <== marked",
939 " > C",
940 " .dockerignore",
941 "v root2",
942 " > d",
943 " > e",
944 ]
945 );
946}
947
948#[gpui::test]
949async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) {
950 init_test(cx);
951
952 let fs = FakeFs::new(cx.executor().clone());
953 fs.insert_tree(
954 path!("/root1"),
955 json!({
956 ".dockerignore": "",
957 ".git": {
958 "HEAD": "",
959 },
960 }),
961 )
962 .await;
963
964 let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await;
965 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
966 let cx = &mut VisualTestContext::from_window(*workspace, cx);
967 let panel = workspace
968 .update(cx, |workspace, window, cx| {
969 let panel = ProjectPanel::new(workspace, window, cx);
970 workspace.add_panel(panel.clone(), window, cx);
971 panel
972 })
973 .unwrap();
974
975 select_path(&panel, "root1", cx);
976 assert_eq!(
977 visible_entries_as_strings(&panel, 0..10, cx),
978 &["v root1 <== selected", " > .git", " .dockerignore",]
979 );
980
981 // Add a file with the root folder selected. The filename editor is placed
982 // before the first file in the root folder.
983 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
984 panel.update_in(cx, |panel, window, cx| {
985 assert!(panel.filename_editor.read(cx).is_focused(window));
986 });
987 assert_eq!(
988 visible_entries_as_strings(&panel, 0..10, cx),
989 &[
990 "v root1",
991 " > .git",
992 " [EDITOR: ''] <== selected",
993 " .dockerignore",
994 ]
995 );
996
997 let confirm = panel.update_in(cx, |panel, window, cx| {
998 // If we want to create a subdirectory, there should be no prefix slash.
999 panel
1000 .filename_editor
1001 .update(cx, |editor, cx| editor.set_text("new_dir/", window, cx));
1002 panel.confirm_edit(window, cx).unwrap()
1003 });
1004
1005 assert_eq!(
1006 visible_entries_as_strings(&panel, 0..10, cx),
1007 &[
1008 "v root1",
1009 " > .git",
1010 " [PROCESSING: 'new_dir/'] <== selected",
1011 " .dockerignore",
1012 ]
1013 );
1014
1015 confirm.await.unwrap();
1016 assert_eq!(
1017 visible_entries_as_strings(&panel, 0..10, cx),
1018 &[
1019 "v root1",
1020 " > .git",
1021 " v new_dir <== selected",
1022 " .dockerignore",
1023 ]
1024 );
1025
1026 // Test filename with whitespace
1027 select_path(&panel, "root1", cx);
1028 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
1029 let confirm = panel.update_in(cx, |panel, window, cx| {
1030 // If we want to create a subdirectory, there should be no prefix slash.
1031 panel
1032 .filename_editor
1033 .update(cx, |editor, cx| editor.set_text("new dir 2/", window, cx));
1034 panel.confirm_edit(window, cx).unwrap()
1035 });
1036 confirm.await.unwrap();
1037 assert_eq!(
1038 visible_entries_as_strings(&panel, 0..10, cx),
1039 &[
1040 "v root1",
1041 " > .git",
1042 " v new dir 2 <== selected",
1043 " v new_dir",
1044 " .dockerignore",
1045 ]
1046 );
1047
1048 // Test filename ends with "\"
1049 #[cfg(target_os = "windows")]
1050 {
1051 select_path(&panel, "root1", cx);
1052 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
1053 let confirm = panel.update_in(cx, |panel, window, cx| {
1054 // If we want to create a subdirectory, there should be no prefix slash.
1055 panel
1056 .filename_editor
1057 .update(cx, |editor, cx| editor.set_text("new_dir_3\\", window, cx));
1058 panel.confirm_edit(window, cx).unwrap()
1059 });
1060 confirm.await.unwrap();
1061 assert_eq!(
1062 visible_entries_as_strings(&panel, 0..10, cx),
1063 &[
1064 "v root1",
1065 " > .git",
1066 " v new dir 2",
1067 " v new_dir",
1068 " v new_dir_3 <== selected",
1069 " .dockerignore",
1070 ]
1071 );
1072 }
1073}
1074
1075#[gpui::test]
1076async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
1077 init_test(cx);
1078
1079 let fs = FakeFs::new(cx.executor().clone());
1080 fs.insert_tree(
1081 "/root1",
1082 json!({
1083 "one.two.txt": "",
1084 "one.txt": ""
1085 }),
1086 )
1087 .await;
1088
1089 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
1090 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1091 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1092 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1093
1094 panel.update_in(cx, |panel, window, cx| {
1095 panel.select_next(&Default::default(), window, cx);
1096 panel.select_next(&Default::default(), window, cx);
1097 });
1098
1099 assert_eq!(
1100 visible_entries_as_strings(&panel, 0..50, cx),
1101 &[
1102 //
1103 "v root1",
1104 " one.txt <== selected",
1105 " one.two.txt",
1106 ]
1107 );
1108
1109 // Regression test - file name is created correctly when
1110 // the copied file's name contains multiple dots.
1111 panel.update_in(cx, |panel, window, cx| {
1112 panel.copy(&Default::default(), window, cx);
1113 panel.paste(&Default::default(), window, cx);
1114 });
1115 cx.executor().run_until_parked();
1116
1117 assert_eq!(
1118 visible_entries_as_strings(&panel, 0..50, cx),
1119 &[
1120 //
1121 "v root1",
1122 " one.txt",
1123 " [EDITOR: 'one copy.txt'] <== selected <== marked",
1124 " one.two.txt",
1125 ]
1126 );
1127
1128 panel.update_in(cx, |panel, window, cx| {
1129 panel.filename_editor.update(cx, |editor, cx| {
1130 let file_name_selections = editor.selections.all::<usize>(cx);
1131 assert_eq!(
1132 file_name_selections.len(),
1133 1,
1134 "File editing should have a single selection, but got: {file_name_selections:?}"
1135 );
1136 let file_name_selection = &file_name_selections[0];
1137 assert_eq!(
1138 file_name_selection.start,
1139 "one".len(),
1140 "Should select the file name disambiguation after the original file name"
1141 );
1142 assert_eq!(
1143 file_name_selection.end,
1144 "one copy".len(),
1145 "Should select the file name disambiguation until the extension"
1146 );
1147 });
1148 assert!(panel.confirm_edit(window, cx).is_none());
1149 });
1150
1151 panel.update_in(cx, |panel, window, cx| {
1152 panel.paste(&Default::default(), window, cx);
1153 });
1154 cx.executor().run_until_parked();
1155
1156 assert_eq!(
1157 visible_entries_as_strings(&panel, 0..50, cx),
1158 &[
1159 //
1160 "v root1",
1161 " one.txt",
1162 " one copy.txt",
1163 " [EDITOR: 'one copy 1.txt'] <== selected <== marked",
1164 " one.two.txt",
1165 ]
1166 );
1167
1168 panel.update_in(cx, |panel, window, cx| {
1169 assert!(panel.confirm_edit(window, cx).is_none())
1170 });
1171}
1172
1173#[gpui::test]
1174async fn test_cut_paste(cx: &mut gpui::TestAppContext) {
1175 init_test(cx);
1176
1177 let fs = FakeFs::new(cx.executor().clone());
1178 fs.insert_tree(
1179 "/root",
1180 json!({
1181 "one.txt": "",
1182 "two.txt": "",
1183 "a": {},
1184 "b": {}
1185 }),
1186 )
1187 .await;
1188
1189 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
1190 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1191 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1192 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1193
1194 select_path_with_mark(&panel, "root/one.txt", cx);
1195 select_path_with_mark(&panel, "root/two.txt", cx);
1196
1197 assert_eq!(
1198 visible_entries_as_strings(&panel, 0..50, cx),
1199 &[
1200 "v root",
1201 " > a",
1202 " > b",
1203 " one.txt <== marked",
1204 " two.txt <== selected <== marked",
1205 ]
1206 );
1207
1208 panel.update_in(cx, |panel, window, cx| {
1209 panel.cut(&Default::default(), window, cx);
1210 });
1211
1212 select_path(&panel, "root/a", cx);
1213
1214 panel.update_in(cx, |panel, window, cx| {
1215 panel.paste(&Default::default(), window, cx);
1216 });
1217 cx.executor().run_until_parked();
1218
1219 assert_eq!(
1220 visible_entries_as_strings(&panel, 0..50, cx),
1221 &[
1222 "v root",
1223 " v a",
1224 " one.txt <== marked",
1225 " two.txt <== selected <== marked",
1226 " > b",
1227 ],
1228 "Cut entries should be moved on first paste."
1229 );
1230
1231 panel.update_in(cx, |panel, window, cx| {
1232 panel.cancel(&menu::Cancel {}, window, cx)
1233 });
1234 cx.executor().run_until_parked();
1235
1236 select_path(&panel, "root/b", cx);
1237
1238 panel.update_in(cx, |panel, window, cx| {
1239 panel.paste(&Default::default(), window, cx);
1240 });
1241 cx.executor().run_until_parked();
1242
1243 assert_eq!(
1244 visible_entries_as_strings(&panel, 0..50, cx),
1245 &[
1246 "v root",
1247 " v a",
1248 " one.txt",
1249 " two.txt",
1250 " v b",
1251 " one.txt",
1252 " two.txt <== selected",
1253 ],
1254 "Cut entries should only be copied for the second paste!"
1255 );
1256}
1257
1258#[gpui::test]
1259async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
1260 init_test(cx);
1261
1262 let fs = FakeFs::new(cx.executor().clone());
1263 fs.insert_tree(
1264 "/root1",
1265 json!({
1266 "one.txt": "",
1267 "two.txt": "",
1268 "three.txt": "",
1269 "a": {
1270 "0": { "q": "", "r": "", "s": "" },
1271 "1": { "t": "", "u": "" },
1272 "2": { "v": "", "w": "", "x": "", "y": "" },
1273 },
1274 }),
1275 )
1276 .await;
1277
1278 fs.insert_tree(
1279 "/root2",
1280 json!({
1281 "one.txt": "",
1282 "two.txt": "",
1283 "four.txt": "",
1284 "b": {
1285 "3": { "Q": "" },
1286 "4": { "R": "", "S": "", "T": "", "U": "" },
1287 },
1288 }),
1289 )
1290 .await;
1291
1292 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1293 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1294 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1295 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1296
1297 select_path(&panel, "root1/three.txt", cx);
1298 panel.update_in(cx, |panel, window, cx| {
1299 panel.cut(&Default::default(), window, cx);
1300 });
1301
1302 select_path(&panel, "root2/one.txt", cx);
1303 panel.update_in(cx, |panel, window, cx| {
1304 panel.select_next(&Default::default(), window, cx);
1305 panel.paste(&Default::default(), window, cx);
1306 });
1307 cx.executor().run_until_parked();
1308 assert_eq!(
1309 visible_entries_as_strings(&panel, 0..50, cx),
1310 &[
1311 //
1312 "v root1",
1313 " > a",
1314 " one.txt",
1315 " two.txt",
1316 "v root2",
1317 " > b",
1318 " four.txt",
1319 " one.txt",
1320 " three.txt <== selected <== marked",
1321 " two.txt",
1322 ]
1323 );
1324
1325 select_path(&panel, "root1/a", cx);
1326 panel.update_in(cx, |panel, window, cx| {
1327 panel.cut(&Default::default(), window, cx);
1328 });
1329 select_path(&panel, "root2/two.txt", cx);
1330 panel.update_in(cx, |panel, window, cx| {
1331 panel.select_next(&Default::default(), window, cx);
1332 panel.paste(&Default::default(), window, cx);
1333 });
1334
1335 cx.executor().run_until_parked();
1336 assert_eq!(
1337 visible_entries_as_strings(&panel, 0..50, cx),
1338 &[
1339 //
1340 "v root1",
1341 " one.txt",
1342 " two.txt",
1343 "v root2",
1344 " > a <== selected",
1345 " > b",
1346 " four.txt",
1347 " one.txt",
1348 " three.txt <== marked",
1349 " two.txt",
1350 ]
1351 );
1352}
1353
1354#[gpui::test]
1355async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
1356 init_test(cx);
1357
1358 let fs = FakeFs::new(cx.executor().clone());
1359 fs.insert_tree(
1360 "/root1",
1361 json!({
1362 "one.txt": "",
1363 "two.txt": "",
1364 "three.txt": "",
1365 "a": {
1366 "0": { "q": "", "r": "", "s": "" },
1367 "1": { "t": "", "u": "" },
1368 "2": { "v": "", "w": "", "x": "", "y": "" },
1369 },
1370 }),
1371 )
1372 .await;
1373
1374 fs.insert_tree(
1375 "/root2",
1376 json!({
1377 "one.txt": "",
1378 "two.txt": "",
1379 "four.txt": "",
1380 "b": {
1381 "3": { "Q": "" },
1382 "4": { "R": "", "S": "", "T": "", "U": "" },
1383 },
1384 }),
1385 )
1386 .await;
1387
1388 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1389 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1390 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1391 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1392
1393 select_path(&panel, "root1/three.txt", cx);
1394 panel.update_in(cx, |panel, window, cx| {
1395 panel.copy(&Default::default(), window, cx);
1396 });
1397
1398 select_path(&panel, "root2/one.txt", cx);
1399 panel.update_in(cx, |panel, window, cx| {
1400 panel.select_next(&Default::default(), window, cx);
1401 panel.paste(&Default::default(), window, cx);
1402 });
1403 cx.executor().run_until_parked();
1404 assert_eq!(
1405 visible_entries_as_strings(&panel, 0..50, cx),
1406 &[
1407 //
1408 "v root1",
1409 " > a",
1410 " one.txt",
1411 " three.txt",
1412 " two.txt",
1413 "v root2",
1414 " > b",
1415 " four.txt",
1416 " one.txt",
1417 " three.txt <== selected <== marked",
1418 " two.txt",
1419 ]
1420 );
1421
1422 select_path(&panel, "root1/three.txt", cx);
1423 panel.update_in(cx, |panel, window, cx| {
1424 panel.copy(&Default::default(), window, cx);
1425 });
1426 select_path(&panel, "root2/two.txt", cx);
1427 panel.update_in(cx, |panel, window, cx| {
1428 panel.select_next(&Default::default(), window, cx);
1429 panel.paste(&Default::default(), window, cx);
1430 });
1431
1432 cx.executor().run_until_parked();
1433 assert_eq!(
1434 visible_entries_as_strings(&panel, 0..50, cx),
1435 &[
1436 //
1437 "v root1",
1438 " > a",
1439 " one.txt",
1440 " three.txt",
1441 " two.txt",
1442 "v root2",
1443 " > b",
1444 " four.txt",
1445 " one.txt",
1446 " three.txt",
1447 " [EDITOR: 'three copy.txt'] <== selected <== marked",
1448 " two.txt",
1449 ]
1450 );
1451
1452 panel.update_in(cx, |panel, window, cx| {
1453 panel.cancel(&menu::Cancel {}, window, cx)
1454 });
1455 cx.executor().run_until_parked();
1456
1457 select_path(&panel, "root1/a", cx);
1458 panel.update_in(cx, |panel, window, cx| {
1459 panel.copy(&Default::default(), window, cx);
1460 });
1461 select_path(&panel, "root2/two.txt", cx);
1462 panel.update_in(cx, |panel, window, cx| {
1463 panel.select_next(&Default::default(), window, cx);
1464 panel.paste(&Default::default(), window, cx);
1465 });
1466
1467 cx.executor().run_until_parked();
1468 assert_eq!(
1469 visible_entries_as_strings(&panel, 0..50, cx),
1470 &[
1471 //
1472 "v root1",
1473 " > a",
1474 " one.txt",
1475 " three.txt",
1476 " two.txt",
1477 "v root2",
1478 " > a <== selected",
1479 " > b",
1480 " four.txt",
1481 " one.txt",
1482 " three.txt",
1483 " three copy.txt",
1484 " two.txt",
1485 ]
1486 );
1487}
1488
1489#[gpui::test]
1490async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) {
1491 init_test(cx);
1492
1493 let fs = FakeFs::new(cx.executor().clone());
1494 fs.insert_tree(
1495 "/root",
1496 json!({
1497 "a": {
1498 "one.txt": "",
1499 "two.txt": "",
1500 "inner_dir": {
1501 "three.txt": "",
1502 "four.txt": "",
1503 }
1504 },
1505 "b": {}
1506 }),
1507 )
1508 .await;
1509
1510 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
1511 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1512 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1513 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1514
1515 select_path(&panel, "root/a", cx);
1516 panel.update_in(cx, |panel, window, cx| {
1517 panel.copy(&Default::default(), window, cx);
1518 panel.select_next(&Default::default(), window, cx);
1519 panel.paste(&Default::default(), window, cx);
1520 });
1521 cx.executor().run_until_parked();
1522
1523 let pasted_dir = find_project_entry(&panel, "root/b/a", cx);
1524 assert_ne!(pasted_dir, None, "Pasted directory should have an entry");
1525
1526 let pasted_dir_file = find_project_entry(&panel, "root/b/a/one.txt", cx);
1527 assert_ne!(
1528 pasted_dir_file, None,
1529 "Pasted directory file should have an entry"
1530 );
1531
1532 let pasted_dir_inner_dir = find_project_entry(&panel, "root/b/a/inner_dir", cx);
1533 assert_ne!(
1534 pasted_dir_inner_dir, None,
1535 "Directories inside pasted directory should have an entry"
1536 );
1537
1538 toggle_expand_dir(&panel, "root/b/a", cx);
1539 toggle_expand_dir(&panel, "root/b/a/inner_dir", cx);
1540
1541 assert_eq!(
1542 visible_entries_as_strings(&panel, 0..50, cx),
1543 &[
1544 //
1545 "v root",
1546 " > a",
1547 " v b",
1548 " v a",
1549 " v inner_dir <== selected",
1550 " four.txt",
1551 " three.txt",
1552 " one.txt",
1553 " two.txt",
1554 ]
1555 );
1556
1557 select_path(&panel, "root", cx);
1558 panel.update_in(cx, |panel, window, cx| {
1559 panel.paste(&Default::default(), window, cx)
1560 });
1561 cx.executor().run_until_parked();
1562 assert_eq!(
1563 visible_entries_as_strings(&panel, 0..50, cx),
1564 &[
1565 //
1566 "v root",
1567 " > a",
1568 " > [EDITOR: 'a copy'] <== selected",
1569 " v b",
1570 " v a",
1571 " v inner_dir",
1572 " four.txt",
1573 " three.txt",
1574 " one.txt",
1575 " two.txt"
1576 ]
1577 );
1578
1579 let confirm = panel.update_in(cx, |panel, window, cx| {
1580 panel
1581 .filename_editor
1582 .update(cx, |editor, cx| editor.set_text("c", window, cx));
1583 panel.confirm_edit(window, cx).unwrap()
1584 });
1585 assert_eq!(
1586 visible_entries_as_strings(&panel, 0..50, cx),
1587 &[
1588 //
1589 "v root",
1590 " > a",
1591 " > [PROCESSING: 'c'] <== selected",
1592 " v b",
1593 " v a",
1594 " v inner_dir",
1595 " four.txt",
1596 " three.txt",
1597 " one.txt",
1598 " two.txt"
1599 ]
1600 );
1601
1602 confirm.await.unwrap();
1603
1604 panel.update_in(cx, |panel, window, cx| {
1605 panel.paste(&Default::default(), window, cx)
1606 });
1607 cx.executor().run_until_parked();
1608 assert_eq!(
1609 visible_entries_as_strings(&panel, 0..50, cx),
1610 &[
1611 //
1612 "v root",
1613 " > a",
1614 " v b",
1615 " v a",
1616 " v inner_dir",
1617 " four.txt",
1618 " three.txt",
1619 " one.txt",
1620 " two.txt",
1621 " v c",
1622 " > a <== selected",
1623 " > inner_dir",
1624 " one.txt",
1625 " two.txt",
1626 ]
1627 );
1628}
1629
1630#[gpui::test]
1631async fn test_copy_paste_directory_with_sibling_file(cx: &mut gpui::TestAppContext) {
1632 init_test(cx);
1633
1634 let fs = FakeFs::new(cx.executor().clone());
1635 fs.insert_tree(
1636 "/test",
1637 json!({
1638 "dir1": {
1639 "a.txt": "",
1640 "b.txt": "",
1641 },
1642 "dir2": {},
1643 "c.txt": "",
1644 "d.txt": "",
1645 }),
1646 )
1647 .await;
1648
1649 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1650 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1651 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1652 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1653
1654 toggle_expand_dir(&panel, "test/dir1", cx);
1655
1656 cx.simulate_modifiers_change(gpui::Modifiers {
1657 control: true,
1658 ..Default::default()
1659 });
1660
1661 select_path_with_mark(&panel, "test/dir1", cx);
1662 select_path_with_mark(&panel, "test/c.txt", cx);
1663
1664 assert_eq!(
1665 visible_entries_as_strings(&panel, 0..15, cx),
1666 &[
1667 "v test",
1668 " v dir1 <== marked",
1669 " a.txt",
1670 " b.txt",
1671 " > dir2",
1672 " c.txt <== selected <== marked",
1673 " d.txt",
1674 ],
1675 "Initial state before copying dir1 and c.txt"
1676 );
1677
1678 panel.update_in(cx, |panel, window, cx| {
1679 panel.copy(&Default::default(), window, cx);
1680 });
1681 select_path(&panel, "test/dir2", cx);
1682 panel.update_in(cx, |panel, window, cx| {
1683 panel.paste(&Default::default(), window, cx);
1684 });
1685 cx.executor().run_until_parked();
1686
1687 toggle_expand_dir(&panel, "test/dir2/dir1", cx);
1688
1689 assert_eq!(
1690 visible_entries_as_strings(&panel, 0..15, cx),
1691 &[
1692 "v test",
1693 " v dir1 <== marked",
1694 " a.txt",
1695 " b.txt",
1696 " v dir2",
1697 " v dir1 <== selected",
1698 " a.txt",
1699 " b.txt",
1700 " c.txt",
1701 " c.txt <== marked",
1702 " d.txt",
1703 ],
1704 "Should copy dir1 as well as c.txt into dir2"
1705 );
1706
1707 // Disambiguating multiple files should not open the rename editor.
1708 select_path(&panel, "test/dir2", cx);
1709 panel.update_in(cx, |panel, window, cx| {
1710 panel.paste(&Default::default(), window, cx);
1711 });
1712 cx.executor().run_until_parked();
1713
1714 assert_eq!(
1715 visible_entries_as_strings(&panel, 0..15, cx),
1716 &[
1717 "v test",
1718 " v dir1 <== marked",
1719 " a.txt",
1720 " b.txt",
1721 " v dir2",
1722 " v dir1",
1723 " a.txt",
1724 " b.txt",
1725 " > dir1 copy <== selected",
1726 " c.txt",
1727 " c copy.txt",
1728 " c.txt <== marked",
1729 " d.txt",
1730 ],
1731 "Should copy dir1 as well as c.txt into dir2 and disambiguate them without opening the rename editor"
1732 );
1733}
1734
1735#[gpui::test]
1736async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) {
1737 init_test(cx);
1738
1739 let fs = FakeFs::new(cx.executor().clone());
1740 fs.insert_tree(
1741 "/test",
1742 json!({
1743 "dir1": {
1744 "a.txt": "",
1745 "b.txt": "",
1746 },
1747 "dir2": {},
1748 "c.txt": "",
1749 "d.txt": "",
1750 }),
1751 )
1752 .await;
1753
1754 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1755 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1756 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1757 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1758
1759 toggle_expand_dir(&panel, "test/dir1", cx);
1760
1761 cx.simulate_modifiers_change(gpui::Modifiers {
1762 control: true,
1763 ..Default::default()
1764 });
1765
1766 select_path_with_mark(&panel, "test/dir1/a.txt", cx);
1767 select_path_with_mark(&panel, "test/dir1", cx);
1768 select_path_with_mark(&panel, "test/c.txt", cx);
1769
1770 assert_eq!(
1771 visible_entries_as_strings(&panel, 0..15, cx),
1772 &[
1773 "v test",
1774 " v dir1 <== marked",
1775 " a.txt <== marked",
1776 " b.txt",
1777 " > dir2",
1778 " c.txt <== selected <== marked",
1779 " d.txt",
1780 ],
1781 "Initial state before copying a.txt, dir1 and c.txt"
1782 );
1783
1784 panel.update_in(cx, |panel, window, cx| {
1785 panel.copy(&Default::default(), window, cx);
1786 });
1787 select_path(&panel, "test/dir2", cx);
1788 panel.update_in(cx, |panel, window, cx| {
1789 panel.paste(&Default::default(), window, cx);
1790 });
1791 cx.executor().run_until_parked();
1792
1793 toggle_expand_dir(&panel, "test/dir2/dir1", cx);
1794
1795 assert_eq!(
1796 visible_entries_as_strings(&panel, 0..20, cx),
1797 &[
1798 "v test",
1799 " v dir1 <== marked",
1800 " a.txt <== marked",
1801 " b.txt",
1802 " v dir2",
1803 " v dir1 <== selected",
1804 " a.txt",
1805 " b.txt",
1806 " c.txt",
1807 " c.txt <== marked",
1808 " d.txt",
1809 ],
1810 "Should copy dir1 and c.txt into dir2. a.txt is already present in copied dir1."
1811 );
1812}
1813
1814#[gpui::test]
1815async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
1816 init_test_with_editor(cx);
1817
1818 let fs = FakeFs::new(cx.executor().clone());
1819 fs.insert_tree(
1820 path!("/src"),
1821 json!({
1822 "test": {
1823 "first.rs": "// First Rust file",
1824 "second.rs": "// Second Rust file",
1825 "third.rs": "// Third Rust file",
1826 }
1827 }),
1828 )
1829 .await;
1830
1831 let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await;
1832 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1833 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1834 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1835
1836 toggle_expand_dir(&panel, "src/test", cx);
1837 select_path(&panel, "src/test/first.rs", cx);
1838 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
1839 cx.executor().run_until_parked();
1840 assert_eq!(
1841 visible_entries_as_strings(&panel, 0..10, cx),
1842 &[
1843 "v src",
1844 " v test",
1845 " first.rs <== selected <== marked",
1846 " second.rs",
1847 " third.rs"
1848 ]
1849 );
1850 ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
1851
1852 submit_deletion(&panel, cx);
1853 assert_eq!(
1854 visible_entries_as_strings(&panel, 0..10, cx),
1855 &[
1856 "v src",
1857 " v test",
1858 " second.rs <== selected",
1859 " third.rs"
1860 ],
1861 "Project panel should have no deleted file, no other file is selected in it"
1862 );
1863 ensure_no_open_items_and_panes(&workspace, cx);
1864
1865 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
1866 cx.executor().run_until_parked();
1867 assert_eq!(
1868 visible_entries_as_strings(&panel, 0..10, cx),
1869 &[
1870 "v src",
1871 " v test",
1872 " second.rs <== selected <== marked",
1873 " third.rs"
1874 ]
1875 );
1876 ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
1877
1878 workspace
1879 .update(cx, |workspace, window, cx| {
1880 let active_items = workspace
1881 .panes()
1882 .iter()
1883 .filter_map(|pane| pane.read(cx).active_item())
1884 .collect::<Vec<_>>();
1885 assert_eq!(active_items.len(), 1);
1886 let open_editor = active_items
1887 .into_iter()
1888 .next()
1889 .unwrap()
1890 .downcast::<Editor>()
1891 .expect("Open item should be an editor");
1892 open_editor.update(cx, |editor, cx| {
1893 editor.set_text("Another text!", window, cx)
1894 });
1895 })
1896 .unwrap();
1897 submit_deletion_skipping_prompt(&panel, cx);
1898 assert_eq!(
1899 visible_entries_as_strings(&panel, 0..10, cx),
1900 &["v src", " v test", " third.rs <== selected"],
1901 "Project panel should have no deleted file, with one last file remaining"
1902 );
1903 ensure_no_open_items_and_panes(&workspace, cx);
1904}
1905
1906#[gpui::test]
1907async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
1908 init_test_with_editor(cx);
1909
1910 let fs = FakeFs::new(cx.executor().clone());
1911 fs.insert_tree(
1912 "/src",
1913 json!({
1914 "test": {
1915 "first.rs": "// First Rust file",
1916 "second.rs": "// Second Rust file",
1917 "third.rs": "// Third Rust file",
1918 }
1919 }),
1920 )
1921 .await;
1922
1923 let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
1924 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1925 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1926 let panel = workspace
1927 .update(cx, |workspace, window, cx| {
1928 let panel = ProjectPanel::new(workspace, window, cx);
1929 workspace.add_panel(panel.clone(), window, cx);
1930 panel
1931 })
1932 .unwrap();
1933
1934 select_path(&panel, "src/", cx);
1935 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
1936 cx.executor().run_until_parked();
1937 assert_eq!(
1938 visible_entries_as_strings(&panel, 0..10, cx),
1939 &[
1940 //
1941 "v src <== selected",
1942 " > test"
1943 ]
1944 );
1945 panel.update_in(cx, |panel, window, cx| {
1946 panel.new_directory(&NewDirectory, window, cx)
1947 });
1948 panel.update_in(cx, |panel, window, cx| {
1949 assert!(panel.filename_editor.read(cx).is_focused(window));
1950 });
1951 assert_eq!(
1952 visible_entries_as_strings(&panel, 0..10, cx),
1953 &[
1954 //
1955 "v src",
1956 " > [EDITOR: ''] <== selected",
1957 " > test"
1958 ]
1959 );
1960 panel.update_in(cx, |panel, window, cx| {
1961 panel
1962 .filename_editor
1963 .update(cx, |editor, cx| editor.set_text("test", window, cx));
1964 assert!(
1965 panel.confirm_edit(window, cx).is_none(),
1966 "Should not allow to confirm on conflicting new directory name"
1967 );
1968 });
1969 cx.executor().run_until_parked();
1970 panel.update_in(cx, |panel, window, cx| {
1971 assert!(
1972 panel.edit_state.is_some(),
1973 "Edit state should not be None after conflicting new directory name"
1974 );
1975 panel.cancel(&menu::Cancel, window, cx);
1976 });
1977 assert_eq!(
1978 visible_entries_as_strings(&panel, 0..10, cx),
1979 &[
1980 //
1981 "v src <== selected",
1982 " > test"
1983 ],
1984 "File list should be unchanged after failed folder create confirmation"
1985 );
1986
1987 select_path(&panel, "src/test/", cx);
1988 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
1989 cx.executor().run_until_parked();
1990 assert_eq!(
1991 visible_entries_as_strings(&panel, 0..10, cx),
1992 &[
1993 //
1994 "v src",
1995 " > test <== selected"
1996 ]
1997 );
1998 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
1999 panel.update_in(cx, |panel, window, cx| {
2000 assert!(panel.filename_editor.read(cx).is_focused(window));
2001 });
2002 assert_eq!(
2003 visible_entries_as_strings(&panel, 0..10, cx),
2004 &[
2005 "v src",
2006 " v test",
2007 " [EDITOR: ''] <== selected",
2008 " first.rs",
2009 " second.rs",
2010 " third.rs"
2011 ]
2012 );
2013 panel.update_in(cx, |panel, window, cx| {
2014 panel
2015 .filename_editor
2016 .update(cx, |editor, cx| editor.set_text("first.rs", window, cx));
2017 assert!(
2018 panel.confirm_edit(window, cx).is_none(),
2019 "Should not allow to confirm on conflicting new file name"
2020 );
2021 });
2022 cx.executor().run_until_parked();
2023 panel.update_in(cx, |panel, window, cx| {
2024 assert!(
2025 panel.edit_state.is_some(),
2026 "Edit state should not be None after conflicting new file name"
2027 );
2028 panel.cancel(&menu::Cancel, window, cx);
2029 });
2030 assert_eq!(
2031 visible_entries_as_strings(&panel, 0..10, cx),
2032 &[
2033 "v src",
2034 " v test <== selected",
2035 " first.rs",
2036 " second.rs",
2037 " third.rs"
2038 ],
2039 "File list should be unchanged after failed file create confirmation"
2040 );
2041
2042 select_path(&panel, "src/test/first.rs", cx);
2043 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2044 cx.executor().run_until_parked();
2045 assert_eq!(
2046 visible_entries_as_strings(&panel, 0..10, cx),
2047 &[
2048 "v src",
2049 " v test",
2050 " first.rs <== selected",
2051 " second.rs",
2052 " third.rs"
2053 ],
2054 );
2055 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2056 panel.update_in(cx, |panel, window, cx| {
2057 assert!(panel.filename_editor.read(cx).is_focused(window));
2058 });
2059 assert_eq!(
2060 visible_entries_as_strings(&panel, 0..10, cx),
2061 &[
2062 "v src",
2063 " v test",
2064 " [EDITOR: 'first.rs'] <== selected",
2065 " second.rs",
2066 " third.rs"
2067 ]
2068 );
2069 panel.update_in(cx, |panel, window, cx| {
2070 panel
2071 .filename_editor
2072 .update(cx, |editor, cx| editor.set_text("second.rs", window, cx));
2073 assert!(
2074 panel.confirm_edit(window, cx).is_none(),
2075 "Should not allow to confirm on conflicting file rename"
2076 )
2077 });
2078 cx.executor().run_until_parked();
2079 panel.update_in(cx, |panel, window, cx| {
2080 assert!(
2081 panel.edit_state.is_some(),
2082 "Edit state should not be None after conflicting file rename"
2083 );
2084 panel.cancel(&menu::Cancel, window, cx);
2085 });
2086 assert_eq!(
2087 visible_entries_as_strings(&panel, 0..10, cx),
2088 &[
2089 "v src",
2090 " v test",
2091 " first.rs <== selected",
2092 " second.rs",
2093 " third.rs"
2094 ],
2095 "File list should be unchanged after failed rename confirmation"
2096 );
2097}
2098
2099#[gpui::test]
2100async fn test_select_git_entry(cx: &mut gpui::TestAppContext) {
2101 init_test_with_editor(cx);
2102
2103 let fs = FakeFs::new(cx.executor().clone());
2104 fs.insert_tree(
2105 path!("/root"),
2106 json!({
2107 "tree1": {
2108 ".git": {},
2109 "dir1": {
2110 "modified1.txt": "1",
2111 "unmodified1.txt": "1",
2112 "modified2.txt": "1",
2113 },
2114 "dir2": {
2115 "modified3.txt": "1",
2116 "unmodified2.txt": "1",
2117 },
2118 "modified4.txt": "1",
2119 "unmodified3.txt": "1",
2120 },
2121 "tree2": {
2122 ".git": {},
2123 "dir3": {
2124 "modified5.txt": "1",
2125 "unmodified4.txt": "1",
2126 },
2127 "modified6.txt": "1",
2128 "unmodified5.txt": "1",
2129 }
2130 }),
2131 )
2132 .await;
2133
2134 // Mark files as git modified
2135 fs.set_git_content_for_repo(
2136 path!("/root/tree1/.git").as_ref(),
2137 &[
2138 ("dir1/modified1.txt".into(), "modified".into(), None),
2139 ("dir1/modified2.txt".into(), "modified".into(), None),
2140 ("modified4.txt".into(), "modified".into(), None),
2141 ("dir2/modified3.txt".into(), "modified".into(), None),
2142 ],
2143 );
2144 fs.set_git_content_for_repo(
2145 path!("/root/tree2/.git").as_ref(),
2146 &[
2147 ("dir3/modified5.txt".into(), "modified".into(), None),
2148 ("modified6.txt".into(), "modified".into(), None),
2149 ],
2150 );
2151
2152 let project = Project::test(
2153 fs.clone(),
2154 [path!("/root/tree1").as_ref(), path!("/root/tree2").as_ref()],
2155 cx,
2156 )
2157 .await;
2158
2159 let (scan1_complete, scan2_complete) = project.update(cx, |project, cx| {
2160 let mut worktrees = project.worktrees(cx);
2161 let worktree1 = worktrees.next().unwrap();
2162 let worktree2 = worktrees.next().unwrap();
2163 (
2164 worktree1.read(cx).as_local().unwrap().scan_complete(),
2165 worktree2.read(cx).as_local().unwrap().scan_complete(),
2166 )
2167 });
2168 scan1_complete.await;
2169 scan2_complete.await;
2170 cx.run_until_parked();
2171
2172 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2173 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2174 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2175
2176 // Check initial state
2177 assert_eq!(
2178 visible_entries_as_strings(&panel, 0..15, cx),
2179 &[
2180 "v tree1",
2181 " > .git",
2182 " > dir1",
2183 " > dir2",
2184 " modified4.txt",
2185 " unmodified3.txt",
2186 "v tree2",
2187 " > .git",
2188 " > dir3",
2189 " modified6.txt",
2190 " unmodified5.txt"
2191 ],
2192 );
2193
2194 // Test selecting next modified entry
2195 panel.update_in(cx, |panel, window, cx| {
2196 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2197 });
2198
2199 assert_eq!(
2200 visible_entries_as_strings(&panel, 0..6, cx),
2201 &[
2202 "v tree1",
2203 " > .git",
2204 " v dir1",
2205 " modified1.txt <== selected",
2206 " modified2.txt",
2207 " unmodified1.txt",
2208 ],
2209 );
2210
2211 panel.update_in(cx, |panel, window, cx| {
2212 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2213 });
2214
2215 assert_eq!(
2216 visible_entries_as_strings(&panel, 0..6, cx),
2217 &[
2218 "v tree1",
2219 " > .git",
2220 " v dir1",
2221 " modified1.txt",
2222 " modified2.txt <== selected",
2223 " unmodified1.txt",
2224 ],
2225 );
2226
2227 panel.update_in(cx, |panel, window, cx| {
2228 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2229 });
2230
2231 assert_eq!(
2232 visible_entries_as_strings(&panel, 6..9, cx),
2233 &[
2234 " v dir2",
2235 " modified3.txt <== selected",
2236 " unmodified2.txt",
2237 ],
2238 );
2239
2240 panel.update_in(cx, |panel, window, cx| {
2241 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2242 });
2243
2244 assert_eq!(
2245 visible_entries_as_strings(&panel, 9..11, cx),
2246 &[" modified4.txt <== selected", " unmodified3.txt",],
2247 );
2248
2249 panel.update_in(cx, |panel, window, cx| {
2250 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2251 });
2252
2253 assert_eq!(
2254 visible_entries_as_strings(&panel, 13..16, cx),
2255 &[
2256 " v dir3",
2257 " modified5.txt <== selected",
2258 " unmodified4.txt",
2259 ],
2260 );
2261
2262 panel.update_in(cx, |panel, window, cx| {
2263 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2264 });
2265
2266 assert_eq!(
2267 visible_entries_as_strings(&panel, 16..18, cx),
2268 &[" modified6.txt <== selected", " unmodified5.txt",],
2269 );
2270
2271 // Wraps around to first modified file
2272 panel.update_in(cx, |panel, window, cx| {
2273 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2274 });
2275
2276 assert_eq!(
2277 visible_entries_as_strings(&panel, 0..18, cx),
2278 &[
2279 "v tree1",
2280 " > .git",
2281 " v dir1",
2282 " modified1.txt <== selected",
2283 " modified2.txt",
2284 " unmodified1.txt",
2285 " v dir2",
2286 " modified3.txt",
2287 " unmodified2.txt",
2288 " modified4.txt",
2289 " unmodified3.txt",
2290 "v tree2",
2291 " > .git",
2292 " v dir3",
2293 " modified5.txt",
2294 " unmodified4.txt",
2295 " modified6.txt",
2296 " unmodified5.txt",
2297 ],
2298 );
2299
2300 // Wraps around again to last modified file
2301 panel.update_in(cx, |panel, window, cx| {
2302 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2303 });
2304
2305 assert_eq!(
2306 visible_entries_as_strings(&panel, 16..18, cx),
2307 &[" modified6.txt <== selected", " unmodified5.txt",],
2308 );
2309
2310 panel.update_in(cx, |panel, window, cx| {
2311 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2312 });
2313
2314 assert_eq!(
2315 visible_entries_as_strings(&panel, 13..16, cx),
2316 &[
2317 " v dir3",
2318 " modified5.txt <== selected",
2319 " unmodified4.txt",
2320 ],
2321 );
2322
2323 panel.update_in(cx, |panel, window, cx| {
2324 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2325 });
2326
2327 assert_eq!(
2328 visible_entries_as_strings(&panel, 9..11, cx),
2329 &[" modified4.txt <== selected", " unmodified3.txt",],
2330 );
2331
2332 panel.update_in(cx, |panel, window, cx| {
2333 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2334 });
2335
2336 assert_eq!(
2337 visible_entries_as_strings(&panel, 6..9, cx),
2338 &[
2339 " v dir2",
2340 " modified3.txt <== selected",
2341 " unmodified2.txt",
2342 ],
2343 );
2344
2345 panel.update_in(cx, |panel, window, cx| {
2346 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2347 });
2348
2349 assert_eq!(
2350 visible_entries_as_strings(&panel, 0..6, cx),
2351 &[
2352 "v tree1",
2353 " > .git",
2354 " v dir1",
2355 " modified1.txt",
2356 " modified2.txt <== selected",
2357 " unmodified1.txt",
2358 ],
2359 );
2360
2361 panel.update_in(cx, |panel, window, cx| {
2362 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2363 });
2364
2365 assert_eq!(
2366 visible_entries_as_strings(&panel, 0..6, cx),
2367 &[
2368 "v tree1",
2369 " > .git",
2370 " v dir1",
2371 " modified1.txt <== selected",
2372 " modified2.txt",
2373 " unmodified1.txt",
2374 ],
2375 );
2376}
2377
2378#[gpui::test]
2379async fn test_select_directory(cx: &mut gpui::TestAppContext) {
2380 init_test_with_editor(cx);
2381
2382 let fs = FakeFs::new(cx.executor().clone());
2383 fs.insert_tree(
2384 "/project_root",
2385 json!({
2386 "dir_1": {
2387 "nested_dir": {
2388 "file_a.py": "# File contents",
2389 }
2390 },
2391 "file_1.py": "# File contents",
2392 "dir_2": {
2393
2394 },
2395 "dir_3": {
2396
2397 },
2398 "file_2.py": "# File contents",
2399 "dir_4": {
2400
2401 },
2402 }),
2403 )
2404 .await;
2405
2406 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2407 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2408 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2409 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2410
2411 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2412 cx.executor().run_until_parked();
2413 select_path(&panel, "project_root/dir_1", cx);
2414 cx.executor().run_until_parked();
2415 assert_eq!(
2416 visible_entries_as_strings(&panel, 0..10, cx),
2417 &[
2418 "v project_root",
2419 " > dir_1 <== selected",
2420 " > dir_2",
2421 " > dir_3",
2422 " > dir_4",
2423 " file_1.py",
2424 " file_2.py",
2425 ]
2426 );
2427 panel.update_in(cx, |panel, window, cx| {
2428 panel.select_prev_directory(&SelectPrevDirectory, window, cx)
2429 });
2430
2431 assert_eq!(
2432 visible_entries_as_strings(&panel, 0..10, cx),
2433 &[
2434 "v project_root <== selected",
2435 " > dir_1",
2436 " > dir_2",
2437 " > dir_3",
2438 " > dir_4",
2439 " file_1.py",
2440 " file_2.py",
2441 ]
2442 );
2443
2444 panel.update_in(cx, |panel, window, cx| {
2445 panel.select_prev_directory(&SelectPrevDirectory, window, cx)
2446 });
2447
2448 assert_eq!(
2449 visible_entries_as_strings(&panel, 0..10, cx),
2450 &[
2451 "v project_root",
2452 " > dir_1",
2453 " > dir_2",
2454 " > dir_3",
2455 " > dir_4 <== selected",
2456 " file_1.py",
2457 " file_2.py",
2458 ]
2459 );
2460
2461 panel.update_in(cx, |panel, window, cx| {
2462 panel.select_next_directory(&SelectNextDirectory, window, cx)
2463 });
2464
2465 assert_eq!(
2466 visible_entries_as_strings(&panel, 0..10, cx),
2467 &[
2468 "v project_root <== selected",
2469 " > dir_1",
2470 " > dir_2",
2471 " > dir_3",
2472 " > dir_4",
2473 " file_1.py",
2474 " file_2.py",
2475 ]
2476 );
2477}
2478#[gpui::test]
2479async fn test_select_first_last(cx: &mut gpui::TestAppContext) {
2480 init_test_with_editor(cx);
2481
2482 let fs = FakeFs::new(cx.executor().clone());
2483 fs.insert_tree(
2484 "/project_root",
2485 json!({
2486 "dir_1": {
2487 "nested_dir": {
2488 "file_a.py": "# File contents",
2489 }
2490 },
2491 "file_1.py": "# File contents",
2492 "file_2.py": "# File contents",
2493 "zdir_2": {
2494 "nested_dir2": {
2495 "file_b.py": "# File contents",
2496 }
2497 },
2498 }),
2499 )
2500 .await;
2501
2502 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2503 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2504 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2505 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2506
2507 assert_eq!(
2508 visible_entries_as_strings(&panel, 0..10, cx),
2509 &[
2510 "v project_root",
2511 " > dir_1",
2512 " > zdir_2",
2513 " file_1.py",
2514 " file_2.py",
2515 ]
2516 );
2517 panel.update_in(cx, |panel, window, cx| {
2518 panel.select_first(&SelectFirst, window, cx)
2519 });
2520
2521 assert_eq!(
2522 visible_entries_as_strings(&panel, 0..10, cx),
2523 &[
2524 "v project_root <== selected",
2525 " > dir_1",
2526 " > zdir_2",
2527 " file_1.py",
2528 " file_2.py",
2529 ]
2530 );
2531
2532 panel.update_in(cx, |panel, window, cx| {
2533 panel.select_last(&SelectLast, window, cx)
2534 });
2535
2536 assert_eq!(
2537 visible_entries_as_strings(&panel, 0..10, cx),
2538 &[
2539 "v project_root",
2540 " > dir_1",
2541 " > zdir_2",
2542 " file_1.py",
2543 " file_2.py <== selected",
2544 ]
2545 );
2546}
2547
2548#[gpui::test]
2549async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) {
2550 init_test_with_editor(cx);
2551
2552 let fs = FakeFs::new(cx.executor().clone());
2553 fs.insert_tree(
2554 "/project_root",
2555 json!({
2556 "dir_1": {
2557 "nested_dir": {
2558 "file_a.py": "# File contents",
2559 }
2560 },
2561 "file_1.py": "# File contents",
2562 }),
2563 )
2564 .await;
2565
2566 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2567 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2568 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2569 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2570
2571 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2572 cx.executor().run_until_parked();
2573 select_path(&panel, "project_root/dir_1", cx);
2574 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2575 select_path(&panel, "project_root/dir_1/nested_dir", cx);
2576 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2577 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2578 cx.executor().run_until_parked();
2579 assert_eq!(
2580 visible_entries_as_strings(&panel, 0..10, cx),
2581 &[
2582 "v project_root",
2583 " v dir_1",
2584 " > nested_dir <== selected",
2585 " file_1.py",
2586 ]
2587 );
2588}
2589
2590#[gpui::test]
2591async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
2592 init_test_with_editor(cx);
2593
2594 let fs = FakeFs::new(cx.executor().clone());
2595 fs.insert_tree(
2596 "/project_root",
2597 json!({
2598 "dir_1": {
2599 "nested_dir": {
2600 "file_a.py": "# File contents",
2601 "file_b.py": "# File contents",
2602 "file_c.py": "# File contents",
2603 },
2604 "file_1.py": "# File contents",
2605 "file_2.py": "# File contents",
2606 "file_3.py": "# File contents",
2607 },
2608 "dir_2": {
2609 "file_1.py": "# File contents",
2610 "file_2.py": "# File contents",
2611 "file_3.py": "# File contents",
2612 }
2613 }),
2614 )
2615 .await;
2616
2617 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2618 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2619 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2620 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2621
2622 panel.update_in(cx, |panel, window, cx| {
2623 panel.collapse_all_entries(&CollapseAllEntries, window, cx)
2624 });
2625 cx.executor().run_until_parked();
2626 assert_eq!(
2627 visible_entries_as_strings(&panel, 0..10, cx),
2628 &["v project_root", " > dir_1", " > dir_2",]
2629 );
2630
2631 // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
2632 toggle_expand_dir(&panel, "project_root/dir_1", cx);
2633 cx.executor().run_until_parked();
2634 assert_eq!(
2635 visible_entries_as_strings(&panel, 0..10, cx),
2636 &[
2637 "v project_root",
2638 " v dir_1 <== selected",
2639 " > nested_dir",
2640 " file_1.py",
2641 " file_2.py",
2642 " file_3.py",
2643 " > dir_2",
2644 ]
2645 );
2646}
2647
2648#[gpui::test]
2649async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
2650 init_test(cx);
2651
2652 let fs = FakeFs::new(cx.executor().clone());
2653 fs.as_fake().insert_tree(path!("/root"), json!({})).await;
2654 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
2655 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2656 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2657 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2658
2659 // Make a new buffer with no backing file
2660 workspace
2661 .update(cx, |workspace, window, cx| {
2662 Editor::new_file(workspace, &Default::default(), window, cx)
2663 })
2664 .unwrap();
2665
2666 cx.executor().run_until_parked();
2667
2668 // "Save as" the buffer, creating a new backing file for it
2669 let save_task = workspace
2670 .update(cx, |workspace, window, cx| {
2671 workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
2672 })
2673 .unwrap();
2674
2675 cx.executor().run_until_parked();
2676 cx.simulate_new_path_selection(|_| Some(PathBuf::from(path!("/root/new"))));
2677 save_task.await.unwrap();
2678
2679 // Rename the file
2680 select_path(&panel, "root/new", cx);
2681 assert_eq!(
2682 visible_entries_as_strings(&panel, 0..10, cx),
2683 &["v root", " new <== selected <== marked"]
2684 );
2685 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2686 panel.update_in(cx, |panel, window, cx| {
2687 panel
2688 .filename_editor
2689 .update(cx, |editor, cx| editor.set_text("newer", window, cx));
2690 });
2691 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2692
2693 cx.executor().run_until_parked();
2694 assert_eq!(
2695 visible_entries_as_strings(&panel, 0..10, cx),
2696 &["v root", " newer <== selected"]
2697 );
2698
2699 workspace
2700 .update(cx, |workspace, window, cx| {
2701 workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
2702 })
2703 .unwrap()
2704 .await
2705 .unwrap();
2706
2707 cx.executor().run_until_parked();
2708 // assert that saving the file doesn't restore "new"
2709 assert_eq!(
2710 visible_entries_as_strings(&panel, 0..10, cx),
2711 &["v root", " newer <== selected"]
2712 );
2713}
2714
2715#[gpui::test]
2716#[cfg_attr(target_os = "windows", ignore)]
2717async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
2718 init_test_with_editor(cx);
2719
2720 let fs = FakeFs::new(cx.executor().clone());
2721 fs.insert_tree(
2722 "/root1",
2723 json!({
2724 "dir1": {
2725 "file1.txt": "content 1",
2726 },
2727 }),
2728 )
2729 .await;
2730
2731 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
2732 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2733 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2734 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2735
2736 toggle_expand_dir(&panel, "root1/dir1", cx);
2737
2738 assert_eq!(
2739 visible_entries_as_strings(&panel, 0..20, cx),
2740 &["v root1", " v dir1 <== selected", " file1.txt",],
2741 "Initial state with worktrees"
2742 );
2743
2744 select_path(&panel, "root1", cx);
2745 assert_eq!(
2746 visible_entries_as_strings(&panel, 0..20, cx),
2747 &["v root1 <== selected", " v dir1", " file1.txt",],
2748 );
2749
2750 // Rename root1 to new_root1
2751 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2752
2753 assert_eq!(
2754 visible_entries_as_strings(&panel, 0..20, cx),
2755 &[
2756 "v [EDITOR: 'root1'] <== selected",
2757 " v dir1",
2758 " file1.txt",
2759 ],
2760 );
2761
2762 let confirm = panel.update_in(cx, |panel, window, cx| {
2763 panel
2764 .filename_editor
2765 .update(cx, |editor, cx| editor.set_text("new_root1", window, cx));
2766 panel.confirm_edit(window, cx).unwrap()
2767 });
2768 confirm.await.unwrap();
2769 assert_eq!(
2770 visible_entries_as_strings(&panel, 0..20, cx),
2771 &[
2772 "v new_root1 <== selected",
2773 " v dir1",
2774 " file1.txt",
2775 ],
2776 "Should update worktree name"
2777 );
2778
2779 // Ensure internal paths have been updated
2780 select_path(&panel, "new_root1/dir1/file1.txt", cx);
2781 assert_eq!(
2782 visible_entries_as_strings(&panel, 0..20, cx),
2783 &[
2784 "v new_root1",
2785 " v dir1",
2786 " file1.txt <== selected",
2787 ],
2788 "Files in renamed worktree are selectable"
2789 );
2790}
2791
2792#[gpui::test]
2793async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
2794 init_test_with_editor(cx);
2795 let fs = FakeFs::new(cx.executor().clone());
2796 fs.insert_tree(
2797 "/project_root",
2798 json!({
2799 "dir_1": {
2800 "nested_dir": {
2801 "file_a.py": "# File contents",
2802 }
2803 },
2804 "file_1.py": "# File contents",
2805 }),
2806 )
2807 .await;
2808
2809 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2810 let worktree_id = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
2811 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2812 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2813 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2814 cx.update(|window, cx| {
2815 panel.update(cx, |this, cx| {
2816 this.select_next(&Default::default(), window, cx);
2817 this.expand_selected_entry(&Default::default(), window, cx);
2818 this.expand_selected_entry(&Default::default(), window, cx);
2819 this.select_next(&Default::default(), window, cx);
2820 this.expand_selected_entry(&Default::default(), window, cx);
2821 this.select_next(&Default::default(), window, cx);
2822 })
2823 });
2824 assert_eq!(
2825 visible_entries_as_strings(&panel, 0..10, cx),
2826 &[
2827 "v project_root",
2828 " v dir_1",
2829 " v nested_dir",
2830 " file_a.py <== selected",
2831 " file_1.py",
2832 ]
2833 );
2834 let modifiers_with_shift = gpui::Modifiers {
2835 shift: true,
2836 ..Default::default()
2837 };
2838 cx.run_until_parked();
2839 cx.simulate_modifiers_change(modifiers_with_shift);
2840 cx.update(|window, cx| {
2841 panel.update(cx, |this, cx| {
2842 this.select_next(&Default::default(), window, cx);
2843 })
2844 });
2845 assert_eq!(
2846 visible_entries_as_strings(&panel, 0..10, cx),
2847 &[
2848 "v project_root",
2849 " v dir_1",
2850 " v nested_dir",
2851 " file_a.py",
2852 " file_1.py <== selected <== marked",
2853 ]
2854 );
2855 cx.update(|window, cx| {
2856 panel.update(cx, |this, cx| {
2857 this.select_previous(&Default::default(), window, cx);
2858 })
2859 });
2860 assert_eq!(
2861 visible_entries_as_strings(&panel, 0..10, cx),
2862 &[
2863 "v project_root",
2864 " v dir_1",
2865 " v nested_dir",
2866 " file_a.py <== selected <== marked",
2867 " file_1.py <== marked",
2868 ]
2869 );
2870 cx.update(|window, cx| {
2871 panel.update(cx, |this, cx| {
2872 let drag = DraggedSelection {
2873 active_selection: this.selection.unwrap(),
2874 marked_selections: Arc::new(this.marked_entries.clone()),
2875 };
2876 let target_entry = this
2877 .project
2878 .read(cx)
2879 .entry_for_path(&(worktree_id, "").into(), cx)
2880 .unwrap();
2881 this.drag_onto(&drag, target_entry.id, false, window, cx);
2882 });
2883 });
2884 cx.run_until_parked();
2885 assert_eq!(
2886 visible_entries_as_strings(&panel, 0..10, cx),
2887 &[
2888 "v project_root",
2889 " v dir_1",
2890 " v nested_dir",
2891 " file_1.py <== marked",
2892 " file_a.py <== selected <== marked",
2893 ]
2894 );
2895 // ESC clears out all marks
2896 cx.update(|window, cx| {
2897 panel.update(cx, |this, cx| {
2898 this.cancel(&menu::Cancel, window, cx);
2899 })
2900 });
2901 assert_eq!(
2902 visible_entries_as_strings(&panel, 0..10, cx),
2903 &[
2904 "v project_root",
2905 " v dir_1",
2906 " v nested_dir",
2907 " file_1.py",
2908 " file_a.py <== selected",
2909 ]
2910 );
2911 // ESC clears out all marks
2912 cx.update(|window, cx| {
2913 panel.update(cx, |this, cx| {
2914 this.select_previous(&SelectPrevious, window, cx);
2915 this.select_next(&SelectNext, window, cx);
2916 })
2917 });
2918 assert_eq!(
2919 visible_entries_as_strings(&panel, 0..10, cx),
2920 &[
2921 "v project_root",
2922 " v dir_1",
2923 " v nested_dir",
2924 " file_1.py <== marked",
2925 " file_a.py <== selected <== marked",
2926 ]
2927 );
2928 cx.simulate_modifiers_change(Default::default());
2929 cx.update(|window, cx| {
2930 panel.update(cx, |this, cx| {
2931 this.cut(&Cut, window, cx);
2932 this.select_previous(&SelectPrevious, window, cx);
2933 this.select_previous(&SelectPrevious, window, cx);
2934
2935 this.paste(&Paste, window, cx);
2936 // this.expand_selected_entry(&ExpandSelectedEntry, cx);
2937 })
2938 });
2939 cx.run_until_parked();
2940 assert_eq!(
2941 visible_entries_as_strings(&panel, 0..10, cx),
2942 &[
2943 "v project_root",
2944 " v dir_1",
2945 " v nested_dir",
2946 " file_1.py <== marked",
2947 " file_a.py <== selected <== marked",
2948 ]
2949 );
2950 cx.simulate_modifiers_change(modifiers_with_shift);
2951 cx.update(|window, cx| {
2952 panel.update(cx, |this, cx| {
2953 this.expand_selected_entry(&Default::default(), window, cx);
2954 this.select_next(&SelectNext, window, cx);
2955 this.select_next(&SelectNext, window, cx);
2956 })
2957 });
2958 submit_deletion(&panel, cx);
2959 assert_eq!(
2960 visible_entries_as_strings(&panel, 0..10, cx),
2961 &[
2962 "v project_root",
2963 " v dir_1",
2964 " v nested_dir <== selected",
2965 ]
2966 );
2967}
2968#[gpui::test]
2969async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
2970 init_test_with_editor(cx);
2971 cx.update(|cx| {
2972 cx.update_global::<SettingsStore, _>(|store, cx| {
2973 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
2974 worktree_settings.file_scan_exclusions = Some(Vec::new());
2975 });
2976 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
2977 project_panel_settings.auto_reveal_entries = Some(false)
2978 });
2979 })
2980 });
2981
2982 let fs = FakeFs::new(cx.background_executor.clone());
2983 fs.insert_tree(
2984 "/project_root",
2985 json!({
2986 ".git": {},
2987 ".gitignore": "**/gitignored_dir",
2988 "dir_1": {
2989 "file_1.py": "# File 1_1 contents",
2990 "file_2.py": "# File 1_2 contents",
2991 "file_3.py": "# File 1_3 contents",
2992 "gitignored_dir": {
2993 "file_a.py": "# File contents",
2994 "file_b.py": "# File contents",
2995 "file_c.py": "# File contents",
2996 },
2997 },
2998 "dir_2": {
2999 "file_1.py": "# File 2_1 contents",
3000 "file_2.py": "# File 2_2 contents",
3001 "file_3.py": "# File 2_3 contents",
3002 }
3003 }),
3004 )
3005 .await;
3006
3007 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3008 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3009 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3010 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3011
3012 assert_eq!(
3013 visible_entries_as_strings(&panel, 0..20, cx),
3014 &[
3015 "v project_root",
3016 " > .git",
3017 " > dir_1",
3018 " > dir_2",
3019 " .gitignore",
3020 ]
3021 );
3022
3023 let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
3024 .expect("dir 1 file is not ignored and should have an entry");
3025 let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
3026 .expect("dir 2 file is not ignored and should have an entry");
3027 let gitignored_dir_file =
3028 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3029 assert_eq!(
3030 gitignored_dir_file, None,
3031 "File in the gitignored dir should not have an entry before its dir is toggled"
3032 );
3033
3034 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3035 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3036 cx.executor().run_until_parked();
3037 assert_eq!(
3038 visible_entries_as_strings(&panel, 0..20, cx),
3039 &[
3040 "v project_root",
3041 " > .git",
3042 " v dir_1",
3043 " v gitignored_dir <== selected",
3044 " file_a.py",
3045 " file_b.py",
3046 " file_c.py",
3047 " file_1.py",
3048 " file_2.py",
3049 " file_3.py",
3050 " > dir_2",
3051 " .gitignore",
3052 ],
3053 "Should show gitignored dir file list in the project panel"
3054 );
3055 let gitignored_dir_file =
3056 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
3057 .expect("after gitignored dir got opened, a file entry should be present");
3058
3059 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3060 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3061 assert_eq!(
3062 visible_entries_as_strings(&panel, 0..20, cx),
3063 &[
3064 "v project_root",
3065 " > .git",
3066 " > dir_1 <== selected",
3067 " > dir_2",
3068 " .gitignore",
3069 ],
3070 "Should hide all dir contents again and prepare for the auto reveal test"
3071 );
3072
3073 for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
3074 panel.update(cx, |panel, cx| {
3075 panel.project.update(cx, |_, cx| {
3076 cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
3077 })
3078 });
3079 cx.run_until_parked();
3080 assert_eq!(
3081 visible_entries_as_strings(&panel, 0..20, cx),
3082 &[
3083 "v project_root",
3084 " > .git",
3085 " > dir_1 <== selected",
3086 " > dir_2",
3087 " .gitignore",
3088 ],
3089 "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
3090 );
3091 }
3092
3093 cx.update(|_, cx| {
3094 cx.update_global::<SettingsStore, _>(|store, cx| {
3095 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3096 project_panel_settings.auto_reveal_entries = Some(true)
3097 });
3098 })
3099 });
3100
3101 panel.update(cx, |panel, cx| {
3102 panel.project.update(cx, |_, cx| {
3103 cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
3104 })
3105 });
3106 cx.run_until_parked();
3107 assert_eq!(
3108 visible_entries_as_strings(&panel, 0..20, cx),
3109 &[
3110 "v project_root",
3111 " > .git",
3112 " v dir_1",
3113 " > gitignored_dir",
3114 " file_1.py <== selected <== marked",
3115 " file_2.py",
3116 " file_3.py",
3117 " > dir_2",
3118 " .gitignore",
3119 ],
3120 "When auto reveal is enabled, not ignored dir_1 entry should be revealed"
3121 );
3122
3123 panel.update(cx, |panel, cx| {
3124 panel.project.update(cx, |_, cx| {
3125 cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
3126 })
3127 });
3128 cx.run_until_parked();
3129 assert_eq!(
3130 visible_entries_as_strings(&panel, 0..20, cx),
3131 &[
3132 "v project_root",
3133 " > .git",
3134 " v dir_1",
3135 " > gitignored_dir",
3136 " file_1.py",
3137 " file_2.py",
3138 " file_3.py",
3139 " v dir_2",
3140 " file_1.py <== selected <== marked",
3141 " file_2.py",
3142 " file_3.py",
3143 " .gitignore",
3144 ],
3145 "When auto reveal is enabled, not ignored dir_2 entry should be revealed"
3146 );
3147
3148 panel.update(cx, |panel, cx| {
3149 panel.project.update(cx, |_, cx| {
3150 cx.emit(project::Event::ActiveEntryChanged(Some(
3151 gitignored_dir_file,
3152 )))
3153 })
3154 });
3155 cx.run_until_parked();
3156 assert_eq!(
3157 visible_entries_as_strings(&panel, 0..20, cx),
3158 &[
3159 "v project_root",
3160 " > .git",
3161 " v dir_1",
3162 " > gitignored_dir",
3163 " file_1.py",
3164 " file_2.py",
3165 " file_3.py",
3166 " v dir_2",
3167 " file_1.py <== selected <== marked",
3168 " file_2.py",
3169 " file_3.py",
3170 " .gitignore",
3171 ],
3172 "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
3173 );
3174
3175 panel.update(cx, |panel, cx| {
3176 panel.project.update(cx, |_, cx| {
3177 cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
3178 })
3179 });
3180 cx.run_until_parked();
3181 assert_eq!(
3182 visible_entries_as_strings(&panel, 0..20, cx),
3183 &[
3184 "v project_root",
3185 " > .git",
3186 " v dir_1",
3187 " v gitignored_dir",
3188 " file_a.py <== selected <== marked",
3189 " file_b.py",
3190 " file_c.py",
3191 " file_1.py",
3192 " file_2.py",
3193 " file_3.py",
3194 " v dir_2",
3195 " file_1.py",
3196 " file_2.py",
3197 " file_3.py",
3198 " .gitignore",
3199 ],
3200 "When a gitignored entry is explicitly revealed, it should be shown in the project tree"
3201 );
3202}
3203
3204#[gpui::test]
3205async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
3206 init_test_with_editor(cx);
3207 cx.update(|cx| {
3208 cx.update_global::<SettingsStore, _>(|store, cx| {
3209 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3210 worktree_settings.file_scan_exclusions = Some(Vec::new());
3211 worktree_settings.file_scan_inclusions =
3212 Some(vec!["always_included_but_ignored_dir/*".to_string()]);
3213 });
3214 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3215 project_panel_settings.auto_reveal_entries = Some(false)
3216 });
3217 })
3218 });
3219
3220 let fs = FakeFs::new(cx.background_executor.clone());
3221 fs.insert_tree(
3222 "/project_root",
3223 json!({
3224 ".git": {},
3225 ".gitignore": "**/gitignored_dir\n/always_included_but_ignored_dir",
3226 "dir_1": {
3227 "file_1.py": "# File 1_1 contents",
3228 "file_2.py": "# File 1_2 contents",
3229 "file_3.py": "# File 1_3 contents",
3230 "gitignored_dir": {
3231 "file_a.py": "# File contents",
3232 "file_b.py": "# File contents",
3233 "file_c.py": "# File contents",
3234 },
3235 },
3236 "dir_2": {
3237 "file_1.py": "# File 2_1 contents",
3238 "file_2.py": "# File 2_2 contents",
3239 "file_3.py": "# File 2_3 contents",
3240 },
3241 "always_included_but_ignored_dir": {
3242 "file_a.py": "# File contents",
3243 "file_b.py": "# File contents",
3244 "file_c.py": "# File contents",
3245 },
3246 }),
3247 )
3248 .await;
3249
3250 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3251 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3252 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3253 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3254
3255 assert_eq!(
3256 visible_entries_as_strings(&panel, 0..20, cx),
3257 &[
3258 "v project_root",
3259 " > .git",
3260 " > always_included_but_ignored_dir",
3261 " > dir_1",
3262 " > dir_2",
3263 " .gitignore",
3264 ]
3265 );
3266
3267 let gitignored_dir_file =
3268 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3269 let always_included_but_ignored_dir_file = find_project_entry(
3270 &panel,
3271 "project_root/always_included_but_ignored_dir/file_a.py",
3272 cx,
3273 )
3274 .expect("file that is .gitignored but set to always be included should have an entry");
3275 assert_eq!(
3276 gitignored_dir_file, None,
3277 "File in the gitignored dir should not have an entry unless its directory is toggled"
3278 );
3279
3280 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3281 cx.run_until_parked();
3282 cx.update(|_, cx| {
3283 cx.update_global::<SettingsStore, _>(|store, cx| {
3284 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3285 project_panel_settings.auto_reveal_entries = Some(true)
3286 });
3287 })
3288 });
3289
3290 panel.update(cx, |panel, cx| {
3291 panel.project.update(cx, |_, cx| {
3292 cx.emit(project::Event::ActiveEntryChanged(Some(
3293 always_included_but_ignored_dir_file,
3294 )))
3295 })
3296 });
3297 cx.run_until_parked();
3298
3299 assert_eq!(
3300 visible_entries_as_strings(&panel, 0..20, cx),
3301 &[
3302 "v project_root",
3303 " > .git",
3304 " v always_included_but_ignored_dir",
3305 " file_a.py <== selected <== marked",
3306 " file_b.py",
3307 " file_c.py",
3308 " v dir_1",
3309 " > gitignored_dir",
3310 " file_1.py",
3311 " file_2.py",
3312 " file_3.py",
3313 " > dir_2",
3314 " .gitignore",
3315 ],
3316 "When auto reveal is enabled, a gitignored but always included selected entry should be revealed in the project panel"
3317 );
3318}
3319
3320#[gpui::test]
3321async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
3322 init_test_with_editor(cx);
3323 cx.update(|cx| {
3324 cx.update_global::<SettingsStore, _>(|store, cx| {
3325 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3326 worktree_settings.file_scan_exclusions = Some(Vec::new());
3327 });
3328 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3329 project_panel_settings.auto_reveal_entries = Some(false)
3330 });
3331 })
3332 });
3333
3334 let fs = FakeFs::new(cx.background_executor.clone());
3335 fs.insert_tree(
3336 "/project_root",
3337 json!({
3338 ".git": {},
3339 ".gitignore": "**/gitignored_dir",
3340 "dir_1": {
3341 "file_1.py": "# File 1_1 contents",
3342 "file_2.py": "# File 1_2 contents",
3343 "file_3.py": "# File 1_3 contents",
3344 "gitignored_dir": {
3345 "file_a.py": "# File contents",
3346 "file_b.py": "# File contents",
3347 "file_c.py": "# File contents",
3348 },
3349 },
3350 "dir_2": {
3351 "file_1.py": "# File 2_1 contents",
3352 "file_2.py": "# File 2_2 contents",
3353 "file_3.py": "# File 2_3 contents",
3354 }
3355 }),
3356 )
3357 .await;
3358
3359 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3360 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3361 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3362 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3363
3364 assert_eq!(
3365 visible_entries_as_strings(&panel, 0..20, cx),
3366 &[
3367 "v project_root",
3368 " > .git",
3369 " > dir_1",
3370 " > dir_2",
3371 " .gitignore",
3372 ]
3373 );
3374
3375 let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
3376 .expect("dir 1 file is not ignored and should have an entry");
3377 let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
3378 .expect("dir 2 file is not ignored and should have an entry");
3379 let gitignored_dir_file =
3380 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3381 assert_eq!(
3382 gitignored_dir_file, None,
3383 "File in the gitignored dir should not have an entry before its dir is toggled"
3384 );
3385
3386 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3387 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3388 cx.run_until_parked();
3389 assert_eq!(
3390 visible_entries_as_strings(&panel, 0..20, cx),
3391 &[
3392 "v project_root",
3393 " > .git",
3394 " v dir_1",
3395 " v gitignored_dir <== selected",
3396 " file_a.py",
3397 " file_b.py",
3398 " file_c.py",
3399 " file_1.py",
3400 " file_2.py",
3401 " file_3.py",
3402 " > dir_2",
3403 " .gitignore",
3404 ],
3405 "Should show gitignored dir file list in the project panel"
3406 );
3407 let gitignored_dir_file =
3408 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
3409 .expect("after gitignored dir got opened, a file entry should be present");
3410
3411 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3412 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3413 assert_eq!(
3414 visible_entries_as_strings(&panel, 0..20, cx),
3415 &[
3416 "v project_root",
3417 " > .git",
3418 " > dir_1 <== selected",
3419 " > dir_2",
3420 " .gitignore",
3421 ],
3422 "Should hide all dir contents again and prepare for the explicit reveal test"
3423 );
3424
3425 for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
3426 panel.update(cx, |panel, cx| {
3427 panel.project.update(cx, |_, cx| {
3428 cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
3429 })
3430 });
3431 cx.run_until_parked();
3432 assert_eq!(
3433 visible_entries_as_strings(&panel, 0..20, cx),
3434 &[
3435 "v project_root",
3436 " > .git",
3437 " > dir_1 <== selected",
3438 " > dir_2",
3439 " .gitignore",
3440 ],
3441 "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
3442 );
3443 }
3444
3445 panel.update(cx, |panel, cx| {
3446 panel.project.update(cx, |_, cx| {
3447 cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
3448 })
3449 });
3450 cx.run_until_parked();
3451 assert_eq!(
3452 visible_entries_as_strings(&panel, 0..20, cx),
3453 &[
3454 "v project_root",
3455 " > .git",
3456 " v dir_1",
3457 " > gitignored_dir",
3458 " file_1.py <== selected <== marked",
3459 " file_2.py",
3460 " file_3.py",
3461 " > dir_2",
3462 " .gitignore",
3463 ],
3464 "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
3465 );
3466
3467 panel.update(cx, |panel, cx| {
3468 panel.project.update(cx, |_, cx| {
3469 cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
3470 })
3471 });
3472 cx.run_until_parked();
3473 assert_eq!(
3474 visible_entries_as_strings(&panel, 0..20, cx),
3475 &[
3476 "v project_root",
3477 " > .git",
3478 " v dir_1",
3479 " > gitignored_dir",
3480 " file_1.py",
3481 " file_2.py",
3482 " file_3.py",
3483 " v dir_2",
3484 " file_1.py <== selected <== marked",
3485 " file_2.py",
3486 " file_3.py",
3487 " .gitignore",
3488 ],
3489 "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
3490 );
3491
3492 panel.update(cx, |panel, cx| {
3493 panel.project.update(cx, |_, cx| {
3494 cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
3495 })
3496 });
3497 cx.run_until_parked();
3498 assert_eq!(
3499 visible_entries_as_strings(&panel, 0..20, cx),
3500 &[
3501 "v project_root",
3502 " > .git",
3503 " v dir_1",
3504 " v gitignored_dir",
3505 " file_a.py <== selected <== marked",
3506 " file_b.py",
3507 " file_c.py",
3508 " file_1.py",
3509 " file_2.py",
3510 " file_3.py",
3511 " v dir_2",
3512 " file_1.py",
3513 " file_2.py",
3514 " file_3.py",
3515 " .gitignore",
3516 ],
3517 "With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
3518 );
3519}
3520
3521#[gpui::test]
3522async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
3523 init_test(cx);
3524 cx.update(|cx| {
3525 cx.update_global::<SettingsStore, _>(|store, cx| {
3526 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
3527 project_settings.file_scan_exclusions =
3528 Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
3529 });
3530 });
3531 });
3532
3533 cx.update(|cx| {
3534 register_project_item::<TestProjectItemView>(cx);
3535 });
3536
3537 let fs = FakeFs::new(cx.executor().clone());
3538 fs.insert_tree(
3539 "/root1",
3540 json!({
3541 ".dockerignore": "",
3542 ".git": {
3543 "HEAD": "",
3544 },
3545 }),
3546 )
3547 .await;
3548
3549 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
3550 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3551 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3552 let panel = workspace
3553 .update(cx, |workspace, window, cx| {
3554 let panel = ProjectPanel::new(workspace, window, cx);
3555 workspace.add_panel(panel.clone(), window, cx);
3556 panel
3557 })
3558 .unwrap();
3559
3560 select_path(&panel, "root1", cx);
3561 assert_eq!(
3562 visible_entries_as_strings(&panel, 0..10, cx),
3563 &["v root1 <== selected", " .dockerignore",]
3564 );
3565 workspace
3566 .update(cx, |workspace, _, cx| {
3567 assert!(
3568 workspace.active_item(cx).is_none(),
3569 "Should have no active items in the beginning"
3570 );
3571 })
3572 .unwrap();
3573
3574 let excluded_file_path = ".git/COMMIT_EDITMSG";
3575 let excluded_dir_path = "excluded_dir";
3576
3577 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
3578 panel.update_in(cx, |panel, window, cx| {
3579 assert!(panel.filename_editor.read(cx).is_focused(window));
3580 });
3581 panel
3582 .update_in(cx, |panel, window, cx| {
3583 panel.filename_editor.update(cx, |editor, cx| {
3584 editor.set_text(excluded_file_path, window, cx)
3585 });
3586 panel.confirm_edit(window, cx).unwrap()
3587 })
3588 .await
3589 .unwrap();
3590
3591 assert_eq!(
3592 visible_entries_as_strings(&panel, 0..13, cx),
3593 &["v root1", " .dockerignore"],
3594 "Excluded dir should not be shown after opening a file in it"
3595 );
3596 panel.update_in(cx, |panel, window, cx| {
3597 assert!(
3598 !panel.filename_editor.read(cx).is_focused(window),
3599 "Should have closed the file name editor"
3600 );
3601 });
3602 workspace
3603 .update(cx, |workspace, _, cx| {
3604 let active_entry_path = workspace
3605 .active_item(cx)
3606 .expect("should have opened and activated the excluded item")
3607 .act_as::<TestProjectItemView>(cx)
3608 .expect("should have opened the corresponding project item for the excluded item")
3609 .read(cx)
3610 .path
3611 .clone();
3612 assert_eq!(
3613 active_entry_path.path.as_ref(),
3614 Path::new(excluded_file_path),
3615 "Should open the excluded file"
3616 );
3617
3618 assert!(
3619 workspace.notification_ids().is_empty(),
3620 "Should have no notifications after opening an excluded file"
3621 );
3622 })
3623 .unwrap();
3624 assert!(
3625 fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
3626 "Should have created the excluded file"
3627 );
3628
3629 select_path(&panel, "root1", cx);
3630 panel.update_in(cx, |panel, window, cx| {
3631 panel.new_directory(&NewDirectory, window, cx)
3632 });
3633 panel.update_in(cx, |panel, window, cx| {
3634 assert!(panel.filename_editor.read(cx).is_focused(window));
3635 });
3636 panel
3637 .update_in(cx, |panel, window, cx| {
3638 panel.filename_editor.update(cx, |editor, cx| {
3639 editor.set_text(excluded_file_path, window, cx)
3640 });
3641 panel.confirm_edit(window, cx).unwrap()
3642 })
3643 .await
3644 .unwrap();
3645
3646 assert_eq!(
3647 visible_entries_as_strings(&panel, 0..13, cx),
3648 &["v root1", " .dockerignore"],
3649 "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
3650 );
3651 panel.update_in(cx, |panel, window, cx| {
3652 assert!(
3653 !panel.filename_editor.read(cx).is_focused(window),
3654 "Should have closed the file name editor"
3655 );
3656 });
3657 workspace
3658 .update(cx, |workspace, _, cx| {
3659 let notifications = workspace.notification_ids();
3660 assert_eq!(
3661 notifications.len(),
3662 1,
3663 "Should receive one notification with the error message"
3664 );
3665 workspace.dismiss_notification(notifications.first().unwrap(), cx);
3666 assert!(workspace.notification_ids().is_empty());
3667 })
3668 .unwrap();
3669
3670 select_path(&panel, "root1", cx);
3671 panel.update_in(cx, |panel, window, cx| {
3672 panel.new_directory(&NewDirectory, window, cx)
3673 });
3674 panel.update_in(cx, |panel, window, cx| {
3675 assert!(panel.filename_editor.read(cx).is_focused(window));
3676 });
3677 panel
3678 .update_in(cx, |panel, window, cx| {
3679 panel.filename_editor.update(cx, |editor, cx| {
3680 editor.set_text(excluded_dir_path, window, cx)
3681 });
3682 panel.confirm_edit(window, cx).unwrap()
3683 })
3684 .await
3685 .unwrap();
3686
3687 assert_eq!(
3688 visible_entries_as_strings(&panel, 0..13, cx),
3689 &["v root1", " .dockerignore"],
3690 "Should not change the project panel after trying to create an excluded directory"
3691 );
3692 panel.update_in(cx, |panel, window, cx| {
3693 assert!(
3694 !panel.filename_editor.read(cx).is_focused(window),
3695 "Should have closed the file name editor"
3696 );
3697 });
3698 workspace
3699 .update(cx, |workspace, _, cx| {
3700 let notifications = workspace.notification_ids();
3701 assert_eq!(
3702 notifications.len(),
3703 1,
3704 "Should receive one notification explaining that no directory is actually shown"
3705 );
3706 workspace.dismiss_notification(notifications.first().unwrap(), cx);
3707 assert!(workspace.notification_ids().is_empty());
3708 })
3709 .unwrap();
3710 assert!(
3711 fs.is_dir(Path::new("/root1/excluded_dir")).await,
3712 "Should have created the excluded directory"
3713 );
3714}
3715
3716#[gpui::test]
3717async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) {
3718 init_test_with_editor(cx);
3719
3720 let fs = FakeFs::new(cx.executor().clone());
3721 fs.insert_tree(
3722 "/src",
3723 json!({
3724 "test": {
3725 "first.rs": "// First Rust file",
3726 "second.rs": "// Second Rust file",
3727 "third.rs": "// Third Rust file",
3728 }
3729 }),
3730 )
3731 .await;
3732
3733 let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
3734 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3735 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3736 let panel = workspace
3737 .update(cx, |workspace, window, cx| {
3738 let panel = ProjectPanel::new(workspace, window, cx);
3739 workspace.add_panel(panel.clone(), window, cx);
3740 panel
3741 })
3742 .unwrap();
3743
3744 select_path(&panel, "src/", cx);
3745 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
3746 cx.executor().run_until_parked();
3747 assert_eq!(
3748 visible_entries_as_strings(&panel, 0..10, cx),
3749 &[
3750 //
3751 "v src <== selected",
3752 " > test"
3753 ]
3754 );
3755 panel.update_in(cx, |panel, window, cx| {
3756 panel.new_directory(&NewDirectory, window, cx)
3757 });
3758 panel.update_in(cx, |panel, window, cx| {
3759 assert!(panel.filename_editor.read(cx).is_focused(window));
3760 });
3761 assert_eq!(
3762 visible_entries_as_strings(&panel, 0..10, cx),
3763 &[
3764 //
3765 "v src",
3766 " > [EDITOR: ''] <== selected",
3767 " > test"
3768 ]
3769 );
3770
3771 panel.update_in(cx, |panel, window, cx| {
3772 panel.cancel(&menu::Cancel, window, cx)
3773 });
3774 assert_eq!(
3775 visible_entries_as_strings(&panel, 0..10, cx),
3776 &[
3777 //
3778 "v src <== selected",
3779 " > test"
3780 ]
3781 );
3782}
3783
3784#[gpui::test]
3785async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
3786 init_test_with_editor(cx);
3787
3788 let fs = FakeFs::new(cx.executor().clone());
3789 fs.insert_tree(
3790 "/root",
3791 json!({
3792 "dir1": {
3793 "subdir1": {},
3794 "file1.txt": "",
3795 "file2.txt": "",
3796 },
3797 "dir2": {
3798 "subdir2": {},
3799 "file3.txt": "",
3800 "file4.txt": "",
3801 },
3802 "file5.txt": "",
3803 "file6.txt": "",
3804 }),
3805 )
3806 .await;
3807
3808 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
3809 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3810 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3811 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3812
3813 toggle_expand_dir(&panel, "root/dir1", cx);
3814 toggle_expand_dir(&panel, "root/dir2", cx);
3815
3816 // Test Case 1: Delete middle file in directory
3817 select_path(&panel, "root/dir1/file1.txt", cx);
3818 assert_eq!(
3819 visible_entries_as_strings(&panel, 0..15, cx),
3820 &[
3821 "v root",
3822 " v dir1",
3823 " > subdir1",
3824 " file1.txt <== selected",
3825 " file2.txt",
3826 " v dir2",
3827 " > subdir2",
3828 " file3.txt",
3829 " file4.txt",
3830 " file5.txt",
3831 " file6.txt",
3832 ],
3833 "Initial state before deleting middle file"
3834 );
3835
3836 submit_deletion(&panel, cx);
3837 assert_eq!(
3838 visible_entries_as_strings(&panel, 0..15, cx),
3839 &[
3840 "v root",
3841 " v dir1",
3842 " > subdir1",
3843 " file2.txt <== selected",
3844 " v dir2",
3845 " > subdir2",
3846 " file3.txt",
3847 " file4.txt",
3848 " file5.txt",
3849 " file6.txt",
3850 ],
3851 "Should select next file after deleting middle file"
3852 );
3853
3854 // Test Case 2: Delete last file in directory
3855 submit_deletion(&panel, cx);
3856 assert_eq!(
3857 visible_entries_as_strings(&panel, 0..15, cx),
3858 &[
3859 "v root",
3860 " v dir1",
3861 " > subdir1 <== selected",
3862 " v dir2",
3863 " > subdir2",
3864 " file3.txt",
3865 " file4.txt",
3866 " file5.txt",
3867 " file6.txt",
3868 ],
3869 "Should select next directory when last file is deleted"
3870 );
3871
3872 // Test Case 3: Delete root level file
3873 select_path(&panel, "root/file6.txt", cx);
3874 assert_eq!(
3875 visible_entries_as_strings(&panel, 0..15, cx),
3876 &[
3877 "v root",
3878 " v dir1",
3879 " > subdir1",
3880 " v dir2",
3881 " > subdir2",
3882 " file3.txt",
3883 " file4.txt",
3884 " file5.txt",
3885 " file6.txt <== selected",
3886 ],
3887 "Initial state before deleting root level file"
3888 );
3889
3890 submit_deletion(&panel, cx);
3891 assert_eq!(
3892 visible_entries_as_strings(&panel, 0..15, cx),
3893 &[
3894 "v root",
3895 " v dir1",
3896 " > subdir1",
3897 " v dir2",
3898 " > subdir2",
3899 " file3.txt",
3900 " file4.txt",
3901 " file5.txt <== selected",
3902 ],
3903 "Should select prev entry at root level"
3904 );
3905}
3906
3907#[gpui::test]
3908async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) {
3909 init_test_with_editor(cx);
3910
3911 let fs = FakeFs::new(cx.executor().clone());
3912 fs.insert_tree(
3913 path!("/root"),
3914 json!({
3915 "aa": "// Testing 1",
3916 "bb": "// Testing 2",
3917 "cc": "// Testing 3",
3918 "dd": "// Testing 4",
3919 "ee": "// Testing 5",
3920 "ff": "// Testing 6",
3921 "gg": "// Testing 7",
3922 "hh": "// Testing 8",
3923 "ii": "// Testing 8",
3924 ".gitignore": "bb\ndd\nee\nff\nii\n'",
3925 }),
3926 )
3927 .await;
3928
3929 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3930 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3931 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3932
3933 // Test 1: Auto selection with one gitignored file next to the deleted file
3934 cx.update(|_, cx| {
3935 let settings = *ProjectPanelSettings::get_global(cx);
3936 ProjectPanelSettings::override_global(
3937 ProjectPanelSettings {
3938 hide_gitignore: true,
3939 ..settings
3940 },
3941 cx,
3942 );
3943 });
3944
3945 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3946
3947 select_path(&panel, "root/aa", cx);
3948 assert_eq!(
3949 visible_entries_as_strings(&panel, 0..10, cx),
3950 &[
3951 "v root",
3952 " .gitignore",
3953 " aa <== selected",
3954 " cc",
3955 " gg",
3956 " hh"
3957 ],
3958 "Initial state should hide files on .gitignore"
3959 );
3960
3961 submit_deletion(&panel, cx);
3962
3963 assert_eq!(
3964 visible_entries_as_strings(&panel, 0..10, cx),
3965 &[
3966 "v root",
3967 " .gitignore",
3968 " cc <== selected",
3969 " gg",
3970 " hh"
3971 ],
3972 "Should select next entry not on .gitignore"
3973 );
3974
3975 // Test 2: Auto selection with many gitignored files next to the deleted file
3976 submit_deletion(&panel, cx);
3977 assert_eq!(
3978 visible_entries_as_strings(&panel, 0..10, cx),
3979 &[
3980 "v root",
3981 " .gitignore",
3982 " gg <== selected",
3983 " hh"
3984 ],
3985 "Should select next entry not on .gitignore"
3986 );
3987
3988 // Test 3: Auto selection of entry before deleted file
3989 select_path(&panel, "root/hh", cx);
3990 assert_eq!(
3991 visible_entries_as_strings(&panel, 0..10, cx),
3992 &[
3993 "v root",
3994 " .gitignore",
3995 " gg",
3996 " hh <== selected"
3997 ],
3998 "Should select next entry not on .gitignore"
3999 );
4000 submit_deletion(&panel, cx);
4001 assert_eq!(
4002 visible_entries_as_strings(&panel, 0..10, cx),
4003 &["v root", " .gitignore", " gg <== selected"],
4004 "Should select next entry not on .gitignore"
4005 );
4006}
4007
4008#[gpui::test]
4009async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) {
4010 init_test_with_editor(cx);
4011
4012 let fs = FakeFs::new(cx.executor().clone());
4013 fs.insert_tree(
4014 path!("/root"),
4015 json!({
4016 "dir1": {
4017 "file1": "// Testing",
4018 "file2": "// Testing",
4019 "file3": "// Testing"
4020 },
4021 "aa": "// Testing",
4022 ".gitignore": "file1\nfile3\n",
4023 }),
4024 )
4025 .await;
4026
4027 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4028 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4029 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4030
4031 cx.update(|_, cx| {
4032 let settings = *ProjectPanelSettings::get_global(cx);
4033 ProjectPanelSettings::override_global(
4034 ProjectPanelSettings {
4035 hide_gitignore: true,
4036 ..settings
4037 },
4038 cx,
4039 );
4040 });
4041
4042 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4043
4044 // Test 1: Visible items should exclude files on gitignore
4045 toggle_expand_dir(&panel, "root/dir1", cx);
4046 select_path(&panel, "root/dir1/file2", cx);
4047 assert_eq!(
4048 visible_entries_as_strings(&panel, 0..10, cx),
4049 &[
4050 "v root",
4051 " v dir1",
4052 " file2 <== selected",
4053 " .gitignore",
4054 " aa"
4055 ],
4056 "Initial state should hide files on .gitignore"
4057 );
4058 submit_deletion(&panel, cx);
4059
4060 // Test 2: Auto selection should go to the parent
4061 assert_eq!(
4062 visible_entries_as_strings(&panel, 0..10, cx),
4063 &[
4064 "v root",
4065 " v dir1 <== selected",
4066 " .gitignore",
4067 " aa"
4068 ],
4069 "Initial state should hide files on .gitignore"
4070 );
4071}
4072
4073#[gpui::test]
4074async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
4075 init_test_with_editor(cx);
4076
4077 let fs = FakeFs::new(cx.executor().clone());
4078 fs.insert_tree(
4079 "/root",
4080 json!({
4081 "dir1": {
4082 "subdir1": {
4083 "a.txt": "",
4084 "b.txt": ""
4085 },
4086 "file1.txt": "",
4087 },
4088 "dir2": {
4089 "subdir2": {
4090 "c.txt": "",
4091 "d.txt": ""
4092 },
4093 "file2.txt": "",
4094 },
4095 "file3.txt": "",
4096 }),
4097 )
4098 .await;
4099
4100 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4101 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4102 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4103 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4104
4105 toggle_expand_dir(&panel, "root/dir1", cx);
4106 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4107 toggle_expand_dir(&panel, "root/dir2", cx);
4108 toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
4109
4110 // Test Case 1: Select and delete nested directory with parent
4111 cx.simulate_modifiers_change(gpui::Modifiers {
4112 control: true,
4113 ..Default::default()
4114 });
4115 select_path_with_mark(&panel, "root/dir1/subdir1", cx);
4116 select_path_with_mark(&panel, "root/dir1", cx);
4117
4118 assert_eq!(
4119 visible_entries_as_strings(&panel, 0..15, cx),
4120 &[
4121 "v root",
4122 " v dir1 <== selected <== marked",
4123 " v subdir1 <== marked",
4124 " a.txt",
4125 " b.txt",
4126 " file1.txt",
4127 " v dir2",
4128 " v subdir2",
4129 " c.txt",
4130 " d.txt",
4131 " file2.txt",
4132 " file3.txt",
4133 ],
4134 "Initial state before deleting nested directory with parent"
4135 );
4136
4137 submit_deletion(&panel, cx);
4138 assert_eq!(
4139 visible_entries_as_strings(&panel, 0..15, cx),
4140 &[
4141 "v root",
4142 " v dir2 <== selected",
4143 " v subdir2",
4144 " c.txt",
4145 " d.txt",
4146 " file2.txt",
4147 " file3.txt",
4148 ],
4149 "Should select next directory after deleting directory with parent"
4150 );
4151
4152 // Test Case 2: Select mixed files and directories across levels
4153 select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx);
4154 select_path_with_mark(&panel, "root/dir2/file2.txt", cx);
4155 select_path_with_mark(&panel, "root/file3.txt", cx);
4156
4157 assert_eq!(
4158 visible_entries_as_strings(&panel, 0..15, cx),
4159 &[
4160 "v root",
4161 " v dir2",
4162 " v subdir2",
4163 " c.txt <== marked",
4164 " d.txt",
4165 " file2.txt <== marked",
4166 " file3.txt <== selected <== marked",
4167 ],
4168 "Initial state before deleting"
4169 );
4170
4171 submit_deletion(&panel, cx);
4172 assert_eq!(
4173 visible_entries_as_strings(&panel, 0..15, cx),
4174 &[
4175 "v root",
4176 " v dir2 <== selected",
4177 " v subdir2",
4178 " d.txt",
4179 ],
4180 "Should select sibling directory"
4181 );
4182}
4183
4184#[gpui::test]
4185async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) {
4186 init_test_with_editor(cx);
4187
4188 let fs = FakeFs::new(cx.executor().clone());
4189 fs.insert_tree(
4190 "/root",
4191 json!({
4192 "dir1": {
4193 "subdir1": {
4194 "a.txt": "",
4195 "b.txt": ""
4196 },
4197 "file1.txt": "",
4198 },
4199 "dir2": {
4200 "subdir2": {
4201 "c.txt": "",
4202 "d.txt": ""
4203 },
4204 "file2.txt": "",
4205 },
4206 "file3.txt": "",
4207 "file4.txt": "",
4208 }),
4209 )
4210 .await;
4211
4212 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4213 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4214 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4215 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4216
4217 toggle_expand_dir(&panel, "root/dir1", cx);
4218 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4219 toggle_expand_dir(&panel, "root/dir2", cx);
4220 toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
4221
4222 // Test Case 1: Select all root files and directories
4223 cx.simulate_modifiers_change(gpui::Modifiers {
4224 control: true,
4225 ..Default::default()
4226 });
4227 select_path_with_mark(&panel, "root/dir1", cx);
4228 select_path_with_mark(&panel, "root/dir2", cx);
4229 select_path_with_mark(&panel, "root/file3.txt", cx);
4230 select_path_with_mark(&panel, "root/file4.txt", cx);
4231 assert_eq!(
4232 visible_entries_as_strings(&panel, 0..20, cx),
4233 &[
4234 "v root",
4235 " v dir1 <== marked",
4236 " v subdir1",
4237 " a.txt",
4238 " b.txt",
4239 " file1.txt",
4240 " v dir2 <== marked",
4241 " v subdir2",
4242 " c.txt",
4243 " d.txt",
4244 " file2.txt",
4245 " file3.txt <== marked",
4246 " file4.txt <== selected <== marked",
4247 ],
4248 "State before deleting all contents"
4249 );
4250
4251 submit_deletion(&panel, cx);
4252 assert_eq!(
4253 visible_entries_as_strings(&panel, 0..20, cx),
4254 &["v root <== selected"],
4255 "Only empty root directory should remain after deleting all contents"
4256 );
4257}
4258
4259#[gpui::test]
4260async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) {
4261 init_test_with_editor(cx);
4262
4263 let fs = FakeFs::new(cx.executor().clone());
4264 fs.insert_tree(
4265 "/root",
4266 json!({
4267 "dir1": {
4268 "subdir1": {
4269 "file_a.txt": "content a",
4270 "file_b.txt": "content b",
4271 },
4272 "subdir2": {
4273 "file_c.txt": "content c",
4274 },
4275 "file1.txt": "content 1",
4276 },
4277 "dir2": {
4278 "file2.txt": "content 2",
4279 },
4280 }),
4281 )
4282 .await;
4283
4284 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4285 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4286 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4287 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4288
4289 toggle_expand_dir(&panel, "root/dir1", cx);
4290 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4291 toggle_expand_dir(&panel, "root/dir2", cx);
4292 cx.simulate_modifiers_change(gpui::Modifiers {
4293 control: true,
4294 ..Default::default()
4295 });
4296
4297 // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory
4298 select_path_with_mark(&panel, "root/dir1", cx);
4299 select_path_with_mark(&panel, "root/dir1/subdir1", cx);
4300 select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx);
4301
4302 assert_eq!(
4303 visible_entries_as_strings(&panel, 0..20, cx),
4304 &[
4305 "v root",
4306 " v dir1 <== marked",
4307 " v subdir1 <== marked",
4308 " file_a.txt <== selected <== marked",
4309 " file_b.txt",
4310 " > subdir2",
4311 " file1.txt",
4312 " v dir2",
4313 " file2.txt",
4314 ],
4315 "State with parent dir, subdir, and file selected"
4316 );
4317 submit_deletion(&panel, cx);
4318 assert_eq!(
4319 visible_entries_as_strings(&panel, 0..20, cx),
4320 &["v root", " v dir2 <== selected", " file2.txt",],
4321 "Only dir2 should remain after deletion"
4322 );
4323}
4324
4325#[gpui::test]
4326async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) {
4327 init_test_with_editor(cx);
4328
4329 let fs = FakeFs::new(cx.executor().clone());
4330 // First worktree
4331 fs.insert_tree(
4332 "/root1",
4333 json!({
4334 "dir1": {
4335 "file1.txt": "content 1",
4336 "file2.txt": "content 2",
4337 },
4338 "dir2": {
4339 "file3.txt": "content 3",
4340 },
4341 }),
4342 )
4343 .await;
4344
4345 // Second worktree
4346 fs.insert_tree(
4347 "/root2",
4348 json!({
4349 "dir3": {
4350 "file4.txt": "content 4",
4351 "file5.txt": "content 5",
4352 },
4353 "file6.txt": "content 6",
4354 }),
4355 )
4356 .await;
4357
4358 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
4359 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4360 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4361 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4362
4363 // Expand all directories for testing
4364 toggle_expand_dir(&panel, "root1/dir1", cx);
4365 toggle_expand_dir(&panel, "root1/dir2", cx);
4366 toggle_expand_dir(&panel, "root2/dir3", cx);
4367
4368 // Test Case 1: Delete files across different worktrees
4369 cx.simulate_modifiers_change(gpui::Modifiers {
4370 control: true,
4371 ..Default::default()
4372 });
4373 select_path_with_mark(&panel, "root1/dir1/file1.txt", cx);
4374 select_path_with_mark(&panel, "root2/dir3/file4.txt", cx);
4375
4376 assert_eq!(
4377 visible_entries_as_strings(&panel, 0..20, cx),
4378 &[
4379 "v root1",
4380 " v dir1",
4381 " file1.txt <== marked",
4382 " file2.txt",
4383 " v dir2",
4384 " file3.txt",
4385 "v root2",
4386 " v dir3",
4387 " file4.txt <== selected <== marked",
4388 " file5.txt",
4389 " file6.txt",
4390 ],
4391 "Initial state with files selected from different worktrees"
4392 );
4393
4394 submit_deletion(&panel, cx);
4395 assert_eq!(
4396 visible_entries_as_strings(&panel, 0..20, cx),
4397 &[
4398 "v root1",
4399 " v dir1",
4400 " file2.txt",
4401 " v dir2",
4402 " file3.txt",
4403 "v root2",
4404 " v dir3",
4405 " file5.txt <== selected",
4406 " file6.txt",
4407 ],
4408 "Should select next file in the last worktree after deletion"
4409 );
4410
4411 // Test Case 2: Delete directories from different worktrees
4412 select_path_with_mark(&panel, "root1/dir1", cx);
4413 select_path_with_mark(&panel, "root2/dir3", cx);
4414
4415 assert_eq!(
4416 visible_entries_as_strings(&panel, 0..20, cx),
4417 &[
4418 "v root1",
4419 " v dir1 <== marked",
4420 " file2.txt",
4421 " v dir2",
4422 " file3.txt",
4423 "v root2",
4424 " v dir3 <== selected <== marked",
4425 " file5.txt",
4426 " file6.txt",
4427 ],
4428 "State with directories marked from different worktrees"
4429 );
4430
4431 submit_deletion(&panel, cx);
4432 assert_eq!(
4433 visible_entries_as_strings(&panel, 0..20, cx),
4434 &[
4435 "v root1",
4436 " v dir2",
4437 " file3.txt",
4438 "v root2",
4439 " file6.txt <== selected",
4440 ],
4441 "Should select remaining file in last worktree after directory deletion"
4442 );
4443
4444 // Test Case 4: Delete all remaining files except roots
4445 select_path_with_mark(&panel, "root1/dir2/file3.txt", cx);
4446 select_path_with_mark(&panel, "root2/file6.txt", cx);
4447
4448 assert_eq!(
4449 visible_entries_as_strings(&panel, 0..20, cx),
4450 &[
4451 "v root1",
4452 " v dir2",
4453 " file3.txt <== marked",
4454 "v root2",
4455 " file6.txt <== selected <== marked",
4456 ],
4457 "State with all remaining files marked"
4458 );
4459
4460 submit_deletion(&panel, cx);
4461 assert_eq!(
4462 visible_entries_as_strings(&panel, 0..20, cx),
4463 &["v root1", " v dir2", "v root2 <== selected"],
4464 "Second parent root should be selected after deleting"
4465 );
4466}
4467
4468#[gpui::test]
4469async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) {
4470 init_test_with_editor(cx);
4471
4472 let fs = FakeFs::new(cx.executor().clone());
4473 fs.insert_tree(
4474 "/root",
4475 json!({
4476 "dir1": {
4477 "file1.txt": "",
4478 "file2.txt": "",
4479 "file3.txt": "",
4480 },
4481 "dir2": {
4482 "file4.txt": "",
4483 "file5.txt": "",
4484 },
4485 }),
4486 )
4487 .await;
4488
4489 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4490 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4491 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4492 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4493
4494 toggle_expand_dir(&panel, "root/dir1", cx);
4495 toggle_expand_dir(&panel, "root/dir2", cx);
4496
4497 cx.simulate_modifiers_change(gpui::Modifiers {
4498 control: true,
4499 ..Default::default()
4500 });
4501
4502 select_path_with_mark(&panel, "root/dir1/file2.txt", cx);
4503 select_path(&panel, "root/dir1/file1.txt", cx);
4504
4505 assert_eq!(
4506 visible_entries_as_strings(&panel, 0..15, cx),
4507 &[
4508 "v root",
4509 " v dir1",
4510 " file1.txt <== selected",
4511 " file2.txt <== marked",
4512 " file3.txt",
4513 " v dir2",
4514 " file4.txt",
4515 " file5.txt",
4516 ],
4517 "Initial state with one marked entry and different selection"
4518 );
4519
4520 // Delete should operate on the selected entry (file1.txt)
4521 submit_deletion(&panel, cx);
4522 assert_eq!(
4523 visible_entries_as_strings(&panel, 0..15, cx),
4524 &[
4525 "v root",
4526 " v dir1",
4527 " file2.txt <== selected <== marked",
4528 " file3.txt",
4529 " v dir2",
4530 " file4.txt",
4531 " file5.txt",
4532 ],
4533 "Should delete selected file, not marked file"
4534 );
4535
4536 select_path_with_mark(&panel, "root/dir1/file3.txt", cx);
4537 select_path_with_mark(&panel, "root/dir2/file4.txt", cx);
4538 select_path(&panel, "root/dir2/file5.txt", cx);
4539
4540 assert_eq!(
4541 visible_entries_as_strings(&panel, 0..15, cx),
4542 &[
4543 "v root",
4544 " v dir1",
4545 " file2.txt <== marked",
4546 " file3.txt <== marked",
4547 " v dir2",
4548 " file4.txt <== marked",
4549 " file5.txt <== selected",
4550 ],
4551 "Initial state with multiple marked entries and different selection"
4552 );
4553
4554 // Delete should operate on all marked entries, ignoring the selection
4555 submit_deletion(&panel, cx);
4556 assert_eq!(
4557 visible_entries_as_strings(&panel, 0..15, cx),
4558 &[
4559 "v root",
4560 " v dir1",
4561 " v dir2",
4562 " file5.txt <== selected",
4563 ],
4564 "Should delete all marked files, leaving only the selected file"
4565 );
4566}
4567
4568#[gpui::test]
4569async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) {
4570 init_test_with_editor(cx);
4571
4572 let fs = FakeFs::new(cx.executor().clone());
4573 fs.insert_tree(
4574 "/root_b",
4575 json!({
4576 "dir1": {
4577 "file1.txt": "content 1",
4578 "file2.txt": "content 2",
4579 },
4580 }),
4581 )
4582 .await;
4583
4584 fs.insert_tree(
4585 "/root_c",
4586 json!({
4587 "dir2": {},
4588 }),
4589 )
4590 .await;
4591
4592 let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await;
4593 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4594 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4595 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4596
4597 toggle_expand_dir(&panel, "root_b/dir1", cx);
4598 toggle_expand_dir(&panel, "root_c/dir2", cx);
4599
4600 cx.simulate_modifiers_change(gpui::Modifiers {
4601 control: true,
4602 ..Default::default()
4603 });
4604 select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx);
4605 select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx);
4606
4607 assert_eq!(
4608 visible_entries_as_strings(&panel, 0..20, cx),
4609 &[
4610 "v root_b",
4611 " v dir1",
4612 " file1.txt <== marked",
4613 " file2.txt <== selected <== marked",
4614 "v root_c",
4615 " v dir2",
4616 ],
4617 "Initial state with files marked in root_b"
4618 );
4619
4620 submit_deletion(&panel, cx);
4621 assert_eq!(
4622 visible_entries_as_strings(&panel, 0..20, cx),
4623 &[
4624 "v root_b",
4625 " v dir1 <== selected",
4626 "v root_c",
4627 " v dir2",
4628 ],
4629 "After deletion in root_b as it's last deletion, selection should be in root_b"
4630 );
4631
4632 select_path_with_mark(&panel, "root_c/dir2", cx);
4633
4634 submit_deletion(&panel, cx);
4635 assert_eq!(
4636 visible_entries_as_strings(&panel, 0..20, cx),
4637 &["v root_b", " v dir1", "v root_c <== selected",],
4638 "After deleting from root_c, it should remain in root_c"
4639 );
4640}
4641
4642fn toggle_expand_dir(
4643 panel: &Entity<ProjectPanel>,
4644 path: impl AsRef<Path>,
4645 cx: &mut VisualTestContext,
4646) {
4647 let path = path.as_ref();
4648 panel.update_in(cx, |panel, window, cx| {
4649 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4650 let worktree = worktree.read(cx);
4651 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4652 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
4653 panel.toggle_expanded(entry_id, window, cx);
4654 return;
4655 }
4656 }
4657 panic!("no worktree for path {:?}", path);
4658 });
4659}
4660
4661#[gpui::test]
4662async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) {
4663 init_test_with_editor(cx);
4664
4665 let fs = FakeFs::new(cx.executor().clone());
4666 fs.insert_tree(
4667 path!("/root"),
4668 json!({
4669 ".gitignore": "**/ignored_dir\n**/ignored_nested",
4670 "dir1": {
4671 "empty1": {
4672 "empty2": {
4673 "empty3": {
4674 "file.txt": ""
4675 }
4676 }
4677 },
4678 "subdir1": {
4679 "file1.txt": "",
4680 "file2.txt": "",
4681 "ignored_nested": {
4682 "ignored_file.txt": ""
4683 }
4684 },
4685 "ignored_dir": {
4686 "subdir": {
4687 "deep_file.txt": ""
4688 }
4689 }
4690 }
4691 }),
4692 )
4693 .await;
4694
4695 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4696 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4697 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4698
4699 // Test 1: When auto-fold is enabled
4700 cx.update(|_, cx| {
4701 let settings = *ProjectPanelSettings::get_global(cx);
4702 ProjectPanelSettings::override_global(
4703 ProjectPanelSettings {
4704 auto_fold_dirs: true,
4705 ..settings
4706 },
4707 cx,
4708 );
4709 });
4710
4711 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4712
4713 assert_eq!(
4714 visible_entries_as_strings(&panel, 0..20, cx),
4715 &["v root", " > dir1", " .gitignore",],
4716 "Initial state should show collapsed root structure"
4717 );
4718
4719 toggle_expand_dir(&panel, "root/dir1", cx);
4720 assert_eq!(
4721 visible_entries_as_strings(&panel, 0..20, cx),
4722 &[
4723 separator!("v root"),
4724 separator!(" v dir1 <== selected"),
4725 separator!(" > empty1/empty2/empty3"),
4726 separator!(" > ignored_dir"),
4727 separator!(" > subdir1"),
4728 separator!(" .gitignore"),
4729 ],
4730 "Should show first level with auto-folded dirs and ignored dir visible"
4731 );
4732
4733 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4734 panel.update(cx, |panel, cx| {
4735 let project = panel.project.read(cx);
4736 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4737 panel.expand_all_for_entry(worktree.id(), entry_id, cx);
4738 panel.update_visible_entries(None, cx);
4739 });
4740 cx.run_until_parked();
4741
4742 assert_eq!(
4743 visible_entries_as_strings(&panel, 0..20, cx),
4744 &[
4745 separator!("v root"),
4746 separator!(" v dir1 <== selected"),
4747 separator!(" v empty1"),
4748 separator!(" v empty2"),
4749 separator!(" v empty3"),
4750 separator!(" file.txt"),
4751 separator!(" > ignored_dir"),
4752 separator!(" v subdir1"),
4753 separator!(" > ignored_nested"),
4754 separator!(" file1.txt"),
4755 separator!(" file2.txt"),
4756 separator!(" .gitignore"),
4757 ],
4758 "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested"
4759 );
4760
4761 // Test 2: When auto-fold is disabled
4762 cx.update(|_, cx| {
4763 let settings = *ProjectPanelSettings::get_global(cx);
4764 ProjectPanelSettings::override_global(
4765 ProjectPanelSettings {
4766 auto_fold_dirs: false,
4767 ..settings
4768 },
4769 cx,
4770 );
4771 });
4772
4773 panel.update_in(cx, |panel, window, cx| {
4774 panel.collapse_all_entries(&CollapseAllEntries, window, cx);
4775 });
4776
4777 toggle_expand_dir(&panel, "root/dir1", cx);
4778 assert_eq!(
4779 visible_entries_as_strings(&panel, 0..20, cx),
4780 &[
4781 separator!("v root"),
4782 separator!(" v dir1 <== selected"),
4783 separator!(" > empty1"),
4784 separator!(" > ignored_dir"),
4785 separator!(" > subdir1"),
4786 separator!(" .gitignore"),
4787 ],
4788 "With auto-fold disabled: should show all directories separately"
4789 );
4790
4791 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4792 panel.update(cx, |panel, cx| {
4793 let project = panel.project.read(cx);
4794 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4795 panel.expand_all_for_entry(worktree.id(), entry_id, cx);
4796 panel.update_visible_entries(None, cx);
4797 });
4798 cx.run_until_parked();
4799
4800 assert_eq!(
4801 visible_entries_as_strings(&panel, 0..20, cx),
4802 &[
4803 separator!("v root"),
4804 separator!(" v dir1 <== selected"),
4805 separator!(" v empty1"),
4806 separator!(" v empty2"),
4807 separator!(" v empty3"),
4808 separator!(" file.txt"),
4809 separator!(" > ignored_dir"),
4810 separator!(" v subdir1"),
4811 separator!(" > ignored_nested"),
4812 separator!(" file1.txt"),
4813 separator!(" file2.txt"),
4814 separator!(" .gitignore"),
4815 ],
4816 "After expand_all without auto-fold: should expand all dirs normally, \
4817 expand ignored_dir itself but not its subdirs, and not expand ignored_nested"
4818 );
4819
4820 // Test 3: When explicitly called on ignored directory
4821 let ignored_dir_entry = find_project_entry(&panel, "root/dir1/ignored_dir", cx).unwrap();
4822 panel.update(cx, |panel, cx| {
4823 let project = panel.project.read(cx);
4824 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4825 panel.expand_all_for_entry(worktree.id(), ignored_dir_entry, cx);
4826 panel.update_visible_entries(None, cx);
4827 });
4828 cx.run_until_parked();
4829
4830 assert_eq!(
4831 visible_entries_as_strings(&panel, 0..20, cx),
4832 &[
4833 separator!("v root"),
4834 separator!(" v dir1 <== selected"),
4835 separator!(" v empty1"),
4836 separator!(" v empty2"),
4837 separator!(" v empty3"),
4838 separator!(" file.txt"),
4839 separator!(" v ignored_dir"),
4840 separator!(" v subdir"),
4841 separator!(" deep_file.txt"),
4842 separator!(" v subdir1"),
4843 separator!(" > ignored_nested"),
4844 separator!(" file1.txt"),
4845 separator!(" file2.txt"),
4846 separator!(" .gitignore"),
4847 ],
4848 "After expand_all on ignored_dir: should expand all contents of the ignored directory"
4849 );
4850}
4851
4852#[gpui::test]
4853async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) {
4854 init_test(cx);
4855
4856 let fs = FakeFs::new(cx.executor().clone());
4857 fs.insert_tree(
4858 path!("/root"),
4859 json!({
4860 "dir1": {
4861 "subdir1": {
4862 "nested1": {
4863 "file1.txt": "",
4864 "file2.txt": ""
4865 },
4866 },
4867 "subdir2": {
4868 "file4.txt": ""
4869 }
4870 },
4871 "dir2": {
4872 "single_file": {
4873 "file5.txt": ""
4874 }
4875 }
4876 }),
4877 )
4878 .await;
4879
4880 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4881 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4882 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4883
4884 // Test 1: Basic collapsing
4885 {
4886 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4887
4888 toggle_expand_dir(&panel, "root/dir1", cx);
4889 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4890 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4891 toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
4892
4893 assert_eq!(
4894 visible_entries_as_strings(&panel, 0..20, cx),
4895 &[
4896 separator!("v root"),
4897 separator!(" v dir1"),
4898 separator!(" v subdir1"),
4899 separator!(" v nested1"),
4900 separator!(" file1.txt"),
4901 separator!(" file2.txt"),
4902 separator!(" v subdir2 <== selected"),
4903 separator!(" file4.txt"),
4904 separator!(" > dir2"),
4905 ],
4906 "Initial state with everything expanded"
4907 );
4908
4909 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4910 panel.update(cx, |panel, cx| {
4911 let project = panel.project.read(cx);
4912 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4913 panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4914 panel.update_visible_entries(None, cx);
4915 });
4916
4917 assert_eq!(
4918 visible_entries_as_strings(&panel, 0..20, cx),
4919 &["v root", " > dir1", " > dir2",],
4920 "All subdirs under dir1 should be collapsed"
4921 );
4922 }
4923
4924 // Test 2: With auto-fold enabled
4925 {
4926 cx.update(|_, cx| {
4927 let settings = *ProjectPanelSettings::get_global(cx);
4928 ProjectPanelSettings::override_global(
4929 ProjectPanelSettings {
4930 auto_fold_dirs: true,
4931 ..settings
4932 },
4933 cx,
4934 );
4935 });
4936
4937 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4938
4939 toggle_expand_dir(&panel, "root/dir1", cx);
4940 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4941 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4942
4943 assert_eq!(
4944 visible_entries_as_strings(&panel, 0..20, cx),
4945 &[
4946 separator!("v root"),
4947 separator!(" v dir1"),
4948 separator!(" v subdir1/nested1 <== selected"),
4949 separator!(" file1.txt"),
4950 separator!(" file2.txt"),
4951 separator!(" > subdir2"),
4952 separator!(" > dir2/single_file"),
4953 ],
4954 "Initial state with some dirs expanded"
4955 );
4956
4957 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4958 panel.update(cx, |panel, cx| {
4959 let project = panel.project.read(cx);
4960 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4961 panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4962 });
4963
4964 toggle_expand_dir(&panel, "root/dir1", cx);
4965
4966 assert_eq!(
4967 visible_entries_as_strings(&panel, 0..20, cx),
4968 &[
4969 separator!("v root"),
4970 separator!(" v dir1 <== selected"),
4971 separator!(" > subdir1/nested1"),
4972 separator!(" > subdir2"),
4973 separator!(" > dir2/single_file"),
4974 ],
4975 "Subdirs should be collapsed and folded with auto-fold enabled"
4976 );
4977 }
4978
4979 // Test 3: With auto-fold disabled
4980 {
4981 cx.update(|_, cx| {
4982 let settings = *ProjectPanelSettings::get_global(cx);
4983 ProjectPanelSettings::override_global(
4984 ProjectPanelSettings {
4985 auto_fold_dirs: false,
4986 ..settings
4987 },
4988 cx,
4989 );
4990 });
4991
4992 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4993
4994 toggle_expand_dir(&panel, "root/dir1", cx);
4995 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4996 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4997
4998 assert_eq!(
4999 visible_entries_as_strings(&panel, 0..20, cx),
5000 &[
5001 separator!("v root"),
5002 separator!(" v dir1"),
5003 separator!(" v subdir1"),
5004 separator!(" v nested1 <== selected"),
5005 separator!(" file1.txt"),
5006 separator!(" file2.txt"),
5007 separator!(" > subdir2"),
5008 separator!(" > dir2"),
5009 ],
5010 "Initial state with some dirs expanded and auto-fold disabled"
5011 );
5012
5013 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
5014 panel.update(cx, |panel, cx| {
5015 let project = panel.project.read(cx);
5016 let worktree = project.worktrees(cx).next().unwrap().read(cx);
5017 panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
5018 });
5019
5020 toggle_expand_dir(&panel, "root/dir1", cx);
5021
5022 assert_eq!(
5023 visible_entries_as_strings(&panel, 0..20, cx),
5024 &[
5025 separator!("v root"),
5026 separator!(" v dir1 <== selected"),
5027 separator!(" > subdir1"),
5028 separator!(" > subdir2"),
5029 separator!(" > dir2"),
5030 ],
5031 "Subdirs should be collapsed but not folded with auto-fold disabled"
5032 );
5033 }
5034}
5035
5036#[gpui::test]
5037async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) {
5038 init_test(cx);
5039
5040 let fs = FakeFs::new(cx.executor().clone());
5041 fs.insert_tree(
5042 path!("/root"),
5043 json!({
5044 "dir1": {
5045 "file1.txt": "",
5046 },
5047 }),
5048 )
5049 .await;
5050
5051 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
5052 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5053 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5054
5055 let panel = workspace
5056 .update(cx, |workspace, window, cx| {
5057 let panel = ProjectPanel::new(workspace, window, cx);
5058 workspace.add_panel(panel.clone(), window, cx);
5059 panel
5060 })
5061 .unwrap();
5062
5063 #[rustfmt::skip]
5064 assert_eq!(
5065 visible_entries_as_strings(&panel, 0..20, cx),
5066 &[
5067 separator!("v root"),
5068 separator!(" > dir1"),
5069 ],
5070 "Initial state with nothing selected"
5071 );
5072
5073 panel.update_in(cx, |panel, window, cx| {
5074 panel.new_file(&NewFile, window, cx);
5075 });
5076 panel.update_in(cx, |panel, window, cx| {
5077 assert!(panel.filename_editor.read(cx).is_focused(window));
5078 });
5079 panel
5080 .update_in(cx, |panel, window, cx| {
5081 panel.filename_editor.update(cx, |editor, cx| {
5082 editor.set_text("hello_from_no_selections", window, cx)
5083 });
5084 panel.confirm_edit(window, cx).unwrap()
5085 })
5086 .await
5087 .unwrap();
5088
5089 #[rustfmt::skip]
5090 assert_eq!(
5091 visible_entries_as_strings(&panel, 0..20, cx),
5092 &[
5093 separator!("v root"),
5094 separator!(" > dir1"),
5095 separator!(" hello_from_no_selections <== selected <== marked"),
5096 ],
5097 "A new file is created under the root directory"
5098 );
5099}
5100
5101fn select_path(panel: &Entity<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
5102 let path = path.as_ref();
5103 panel.update(cx, |panel, cx| {
5104 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
5105 let worktree = worktree.read(cx);
5106 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
5107 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
5108 panel.selection = Some(crate::SelectedEntry {
5109 worktree_id: worktree.id(),
5110 entry_id,
5111 });
5112 return;
5113 }
5114 }
5115 panic!("no worktree for path {:?}", path);
5116 });
5117}
5118
5119fn select_path_with_mark(
5120 panel: &Entity<ProjectPanel>,
5121 path: impl AsRef<Path>,
5122 cx: &mut VisualTestContext,
5123) {
5124 let path = path.as_ref();
5125 panel.update(cx, |panel, cx| {
5126 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
5127 let worktree = worktree.read(cx);
5128 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
5129 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
5130 let entry = crate::SelectedEntry {
5131 worktree_id: worktree.id(),
5132 entry_id,
5133 };
5134 if !panel.marked_entries.contains(&entry) {
5135 panel.marked_entries.insert(entry);
5136 }
5137 panel.selection = Some(entry);
5138 return;
5139 }
5140 }
5141 panic!("no worktree for path {:?}", path);
5142 });
5143}
5144
5145fn find_project_entry(
5146 panel: &Entity<ProjectPanel>,
5147 path: impl AsRef<Path>,
5148 cx: &mut VisualTestContext,
5149) -> Option<ProjectEntryId> {
5150 let path = path.as_ref();
5151 panel.update(cx, |panel, cx| {
5152 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
5153 let worktree = worktree.read(cx);
5154 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
5155 return worktree.entry_for_path(relative_path).map(|entry| entry.id);
5156 }
5157 }
5158 panic!("no worktree for path {path:?}");
5159 })
5160}
5161
5162fn visible_entries_as_strings(
5163 panel: &Entity<ProjectPanel>,
5164 range: Range<usize>,
5165 cx: &mut VisualTestContext,
5166) -> Vec<String> {
5167 let mut result = Vec::new();
5168 let mut project_entries = HashSet::default();
5169 let mut has_editor = false;
5170
5171 panel.update_in(cx, |panel, window, cx| {
5172 panel.for_each_visible_entry(range, window, cx, |project_entry, details, _, _| {
5173 if details.is_editing {
5174 assert!(!has_editor, "duplicate editor entry");
5175 has_editor = true;
5176 } else {
5177 assert!(
5178 project_entries.insert(project_entry),
5179 "duplicate project entry {:?} {:?}",
5180 project_entry,
5181 details
5182 );
5183 }
5184
5185 let indent = " ".repeat(details.depth);
5186 let icon = if details.kind.is_dir() {
5187 if details.is_expanded { "v " } else { "> " }
5188 } else {
5189 " "
5190 };
5191 let name = if details.is_editing {
5192 format!("[EDITOR: '{}']", details.filename)
5193 } else if details.is_processing {
5194 format!("[PROCESSING: '{}']", details.filename)
5195 } else {
5196 details.filename.clone()
5197 };
5198 let selected = if details.is_selected {
5199 " <== selected"
5200 } else {
5201 ""
5202 };
5203 let marked = if details.is_marked {
5204 " <== marked"
5205 } else {
5206 ""
5207 };
5208
5209 result.push(format!("{indent}{icon}{name}{selected}{marked}"));
5210 });
5211 });
5212
5213 result
5214}
5215
5216fn init_test(cx: &mut TestAppContext) {
5217 cx.update(|cx| {
5218 let settings_store = SettingsStore::test(cx);
5219 cx.set_global(settings_store);
5220 init_settings(cx);
5221 theme::init(theme::LoadThemes::JustBase, cx);
5222 language::init(cx);
5223 editor::init_settings(cx);
5224 crate::init(cx);
5225 workspace::init_settings(cx);
5226 client::init_settings(cx);
5227 Project::init_settings(cx);
5228
5229 cx.update_global::<SettingsStore, _>(|store, cx| {
5230 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5231 project_panel_settings.auto_fold_dirs = Some(false);
5232 });
5233 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5234 worktree_settings.file_scan_exclusions = Some(Vec::new());
5235 });
5236 });
5237 });
5238}
5239
5240fn init_test_with_editor(cx: &mut TestAppContext) {
5241 cx.update(|cx| {
5242 let app_state = AppState::test(cx);
5243 theme::init(theme::LoadThemes::JustBase, cx);
5244 init_settings(cx);
5245 language::init(cx);
5246 editor::init(cx);
5247 crate::init(cx);
5248 workspace::init(app_state.clone(), cx);
5249 Project::init_settings(cx);
5250
5251 cx.update_global::<SettingsStore, _>(|store, cx| {
5252 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5253 project_panel_settings.auto_fold_dirs = Some(false);
5254 });
5255 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5256 worktree_settings.file_scan_exclusions = Some(Vec::new());
5257 });
5258 });
5259 });
5260}
5261
5262fn ensure_single_file_is_opened(
5263 window: &WindowHandle<Workspace>,
5264 expected_path: &str,
5265 cx: &mut TestAppContext,
5266) {
5267 window
5268 .update(cx, |workspace, _, cx| {
5269 let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
5270 assert_eq!(worktrees.len(), 1);
5271 let worktree_id = worktrees[0].read(cx).id();
5272
5273 let open_project_paths = workspace
5274 .panes()
5275 .iter()
5276 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
5277 .collect::<Vec<_>>();
5278 assert_eq!(
5279 open_project_paths,
5280 vec![ProjectPath {
5281 worktree_id,
5282 path: Arc::from(Path::new(expected_path))
5283 }],
5284 "Should have opened file, selected in project panel"
5285 );
5286 })
5287 .unwrap();
5288}
5289
5290fn submit_deletion(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
5291 assert!(
5292 !cx.has_pending_prompt(),
5293 "Should have no prompts before the deletion"
5294 );
5295 panel.update_in(cx, |panel, window, cx| {
5296 panel.delete(&Delete { skip_prompt: false }, window, cx)
5297 });
5298 assert!(
5299 cx.has_pending_prompt(),
5300 "Should have a prompt after the deletion"
5301 );
5302 cx.simulate_prompt_answer("Delete");
5303 assert!(
5304 !cx.has_pending_prompt(),
5305 "Should have no prompts after prompt was replied to"
5306 );
5307 cx.executor().run_until_parked();
5308}
5309
5310fn submit_deletion_skipping_prompt(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
5311 assert!(
5312 !cx.has_pending_prompt(),
5313 "Should have no prompts before the deletion"
5314 );
5315 panel.update_in(cx, |panel, window, cx| {
5316 panel.delete(&Delete { skip_prompt: true }, window, cx)
5317 });
5318 assert!(!cx.has_pending_prompt(), "Should have received no prompts");
5319 cx.executor().run_until_parked();
5320}
5321
5322fn ensure_no_open_items_and_panes(workspace: &WindowHandle<Workspace>, cx: &mut VisualTestContext) {
5323 assert!(
5324 !cx.has_pending_prompt(),
5325 "Should have no prompts after deletion operation closes the file"
5326 );
5327 workspace
5328 .read_with(cx, |workspace, cx| {
5329 let open_project_paths = workspace
5330 .panes()
5331 .iter()
5332 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
5333 .collect::<Vec<_>>();
5334 assert!(
5335 open_project_paths.is_empty(),
5336 "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
5337 );
5338 })
5339 .unwrap();
5340}
5341
5342struct TestProjectItemView {
5343 focus_handle: FocusHandle,
5344 path: ProjectPath,
5345}
5346
5347struct TestProjectItem {
5348 path: ProjectPath,
5349}
5350
5351impl project::ProjectItem for TestProjectItem {
5352 fn try_open(
5353 _project: &Entity<Project>,
5354 path: &ProjectPath,
5355 cx: &mut App,
5356 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
5357 let path = path.clone();
5358 Some(cx.spawn(async move |cx| cx.new(|_| Self { path })))
5359 }
5360
5361 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
5362 None
5363 }
5364
5365 fn project_path(&self, _: &App) -> Option<ProjectPath> {
5366 Some(self.path.clone())
5367 }
5368
5369 fn is_dirty(&self) -> bool {
5370 false
5371 }
5372}
5373
5374impl ProjectItem for TestProjectItemView {
5375 type Item = TestProjectItem;
5376
5377 fn for_project_item(
5378 _: Entity<Project>,
5379 _: Option<&Pane>,
5380 project_item: Entity<Self::Item>,
5381 _: &mut Window,
5382 cx: &mut Context<Self>,
5383 ) -> Self
5384 where
5385 Self: Sized,
5386 {
5387 Self {
5388 path: project_item.update(cx, |project_item, _| project_item.path.clone()),
5389 focus_handle: cx.focus_handle(),
5390 }
5391 }
5392}
5393
5394impl Item for TestProjectItemView {
5395 type Event = ();
5396
5397 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
5398 "Test".into()
5399 }
5400}
5401
5402impl EventEmitter<()> for TestProjectItemView {}
5403
5404impl Focusable for TestProjectItemView {
5405 fn focus_handle(&self, _: &App) -> FocusHandle {
5406 self.focus_handle.clone()
5407 }
5408}
5409
5410impl Render for TestProjectItemView {
5411 fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5412 Empty
5413 }
5414}