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_between_different_worktrees(cx: &mut gpui::TestAppContext) {
1175 init_test(cx);
1176
1177 let fs = FakeFs::new(cx.executor().clone());
1178 fs.insert_tree(
1179 "/root1",
1180 json!({
1181 "one.txt": "",
1182 "two.txt": "",
1183 "three.txt": "",
1184 "a": {
1185 "0": { "q": "", "r": "", "s": "" },
1186 "1": { "t": "", "u": "" },
1187 "2": { "v": "", "w": "", "x": "", "y": "" },
1188 },
1189 }),
1190 )
1191 .await;
1192
1193 fs.insert_tree(
1194 "/root2",
1195 json!({
1196 "one.txt": "",
1197 "two.txt": "",
1198 "four.txt": "",
1199 "b": {
1200 "3": { "Q": "" },
1201 "4": { "R": "", "S": "", "T": "", "U": "" },
1202 },
1203 }),
1204 )
1205 .await;
1206
1207 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1208 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1209 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1210 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1211
1212 select_path(&panel, "root1/three.txt", cx);
1213 panel.update_in(cx, |panel, window, cx| {
1214 panel.cut(&Default::default(), window, cx);
1215 });
1216
1217 select_path(&panel, "root2/one.txt", cx);
1218 panel.update_in(cx, |panel, window, cx| {
1219 panel.select_next(&Default::default(), window, cx);
1220 panel.paste(&Default::default(), window, cx);
1221 });
1222 cx.executor().run_until_parked();
1223 assert_eq!(
1224 visible_entries_as_strings(&panel, 0..50, cx),
1225 &[
1226 //
1227 "v root1",
1228 " > a",
1229 " one.txt",
1230 " two.txt",
1231 "v root2",
1232 " > b",
1233 " four.txt",
1234 " one.txt",
1235 " three.txt <== selected <== marked",
1236 " two.txt",
1237 ]
1238 );
1239
1240 select_path(&panel, "root1/a", cx);
1241 panel.update_in(cx, |panel, window, cx| {
1242 panel.cut(&Default::default(), window, cx);
1243 });
1244 select_path(&panel, "root2/two.txt", cx);
1245 panel.update_in(cx, |panel, window, cx| {
1246 panel.select_next(&Default::default(), window, cx);
1247 panel.paste(&Default::default(), window, cx);
1248 });
1249
1250 cx.executor().run_until_parked();
1251 assert_eq!(
1252 visible_entries_as_strings(&panel, 0..50, cx),
1253 &[
1254 //
1255 "v root1",
1256 " one.txt",
1257 " two.txt",
1258 "v root2",
1259 " > a <== selected",
1260 " > b",
1261 " four.txt",
1262 " one.txt",
1263 " three.txt <== marked",
1264 " two.txt",
1265 ]
1266 );
1267}
1268
1269#[gpui::test]
1270async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
1271 init_test(cx);
1272
1273 let fs = FakeFs::new(cx.executor().clone());
1274 fs.insert_tree(
1275 "/root1",
1276 json!({
1277 "one.txt": "",
1278 "two.txt": "",
1279 "three.txt": "",
1280 "a": {
1281 "0": { "q": "", "r": "", "s": "" },
1282 "1": { "t": "", "u": "" },
1283 "2": { "v": "", "w": "", "x": "", "y": "" },
1284 },
1285 }),
1286 )
1287 .await;
1288
1289 fs.insert_tree(
1290 "/root2",
1291 json!({
1292 "one.txt": "",
1293 "two.txt": "",
1294 "four.txt": "",
1295 "b": {
1296 "3": { "Q": "" },
1297 "4": { "R": "", "S": "", "T": "", "U": "" },
1298 },
1299 }),
1300 )
1301 .await;
1302
1303 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1304 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1305 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1306 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1307
1308 select_path(&panel, "root1/three.txt", cx);
1309 panel.update_in(cx, |panel, window, cx| {
1310 panel.copy(&Default::default(), window, cx);
1311 });
1312
1313 select_path(&panel, "root2/one.txt", cx);
1314 panel.update_in(cx, |panel, window, cx| {
1315 panel.select_next(&Default::default(), window, cx);
1316 panel.paste(&Default::default(), window, cx);
1317 });
1318 cx.executor().run_until_parked();
1319 assert_eq!(
1320 visible_entries_as_strings(&panel, 0..50, cx),
1321 &[
1322 //
1323 "v root1",
1324 " > a",
1325 " one.txt",
1326 " three.txt",
1327 " two.txt",
1328 "v root2",
1329 " > b",
1330 " four.txt",
1331 " one.txt",
1332 " three.txt <== selected <== marked",
1333 " two.txt",
1334 ]
1335 );
1336
1337 select_path(&panel, "root1/three.txt", cx);
1338 panel.update_in(cx, |panel, window, cx| {
1339 panel.copy(&Default::default(), window, cx);
1340 });
1341 select_path(&panel, "root2/two.txt", cx);
1342 panel.update_in(cx, |panel, window, cx| {
1343 panel.select_next(&Default::default(), window, cx);
1344 panel.paste(&Default::default(), window, cx);
1345 });
1346
1347 cx.executor().run_until_parked();
1348 assert_eq!(
1349 visible_entries_as_strings(&panel, 0..50, cx),
1350 &[
1351 //
1352 "v root1",
1353 " > a",
1354 " one.txt",
1355 " three.txt",
1356 " two.txt",
1357 "v root2",
1358 " > b",
1359 " four.txt",
1360 " one.txt",
1361 " three.txt",
1362 " [EDITOR: 'three copy.txt'] <== selected <== marked",
1363 " two.txt",
1364 ]
1365 );
1366
1367 panel.update_in(cx, |panel, window, cx| {
1368 panel.cancel(&menu::Cancel {}, window, cx)
1369 });
1370 cx.executor().run_until_parked();
1371
1372 select_path(&panel, "root1/a", cx);
1373 panel.update_in(cx, |panel, window, cx| {
1374 panel.copy(&Default::default(), window, cx);
1375 });
1376 select_path(&panel, "root2/two.txt", cx);
1377 panel.update_in(cx, |panel, window, cx| {
1378 panel.select_next(&Default::default(), window, cx);
1379 panel.paste(&Default::default(), window, cx);
1380 });
1381
1382 cx.executor().run_until_parked();
1383 assert_eq!(
1384 visible_entries_as_strings(&panel, 0..50, cx),
1385 &[
1386 //
1387 "v root1",
1388 " > a",
1389 " one.txt",
1390 " three.txt",
1391 " two.txt",
1392 "v root2",
1393 " > a <== selected",
1394 " > b",
1395 " four.txt",
1396 " one.txt",
1397 " three.txt",
1398 " three copy.txt",
1399 " two.txt",
1400 ]
1401 );
1402}
1403
1404#[gpui::test]
1405async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) {
1406 init_test(cx);
1407
1408 let fs = FakeFs::new(cx.executor().clone());
1409 fs.insert_tree(
1410 "/root",
1411 json!({
1412 "a": {
1413 "one.txt": "",
1414 "two.txt": "",
1415 "inner_dir": {
1416 "three.txt": "",
1417 "four.txt": "",
1418 }
1419 },
1420 "b": {}
1421 }),
1422 )
1423 .await;
1424
1425 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
1426 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1427 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1428 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1429
1430 select_path(&panel, "root/a", cx);
1431 panel.update_in(cx, |panel, window, cx| {
1432 panel.copy(&Default::default(), window, cx);
1433 panel.select_next(&Default::default(), window, cx);
1434 panel.paste(&Default::default(), window, cx);
1435 });
1436 cx.executor().run_until_parked();
1437
1438 let pasted_dir = find_project_entry(&panel, "root/b/a", cx);
1439 assert_ne!(pasted_dir, None, "Pasted directory should have an entry");
1440
1441 let pasted_dir_file = find_project_entry(&panel, "root/b/a/one.txt", cx);
1442 assert_ne!(
1443 pasted_dir_file, None,
1444 "Pasted directory file should have an entry"
1445 );
1446
1447 let pasted_dir_inner_dir = find_project_entry(&panel, "root/b/a/inner_dir", cx);
1448 assert_ne!(
1449 pasted_dir_inner_dir, None,
1450 "Directories inside pasted directory should have an entry"
1451 );
1452
1453 toggle_expand_dir(&panel, "root/b/a", cx);
1454 toggle_expand_dir(&panel, "root/b/a/inner_dir", cx);
1455
1456 assert_eq!(
1457 visible_entries_as_strings(&panel, 0..50, cx),
1458 &[
1459 //
1460 "v root",
1461 " > a",
1462 " v b",
1463 " v a",
1464 " v inner_dir <== selected",
1465 " four.txt",
1466 " three.txt",
1467 " one.txt",
1468 " two.txt",
1469 ]
1470 );
1471
1472 select_path(&panel, "root", cx);
1473 panel.update_in(cx, |panel, window, cx| {
1474 panel.paste(&Default::default(), window, cx)
1475 });
1476 cx.executor().run_until_parked();
1477 assert_eq!(
1478 visible_entries_as_strings(&panel, 0..50, cx),
1479 &[
1480 //
1481 "v root",
1482 " > a",
1483 " > [EDITOR: 'a copy'] <== selected",
1484 " v b",
1485 " v a",
1486 " v inner_dir",
1487 " four.txt",
1488 " three.txt",
1489 " one.txt",
1490 " two.txt"
1491 ]
1492 );
1493
1494 let confirm = panel.update_in(cx, |panel, window, cx| {
1495 panel
1496 .filename_editor
1497 .update(cx, |editor, cx| editor.set_text("c", window, cx));
1498 panel.confirm_edit(window, cx).unwrap()
1499 });
1500 assert_eq!(
1501 visible_entries_as_strings(&panel, 0..50, cx),
1502 &[
1503 //
1504 "v root",
1505 " > a",
1506 " > [PROCESSING: 'c'] <== selected",
1507 " v b",
1508 " v a",
1509 " v inner_dir",
1510 " four.txt",
1511 " three.txt",
1512 " one.txt",
1513 " two.txt"
1514 ]
1515 );
1516
1517 confirm.await.unwrap();
1518
1519 panel.update_in(cx, |panel, window, cx| {
1520 panel.paste(&Default::default(), window, cx)
1521 });
1522 cx.executor().run_until_parked();
1523 assert_eq!(
1524 visible_entries_as_strings(&panel, 0..50, cx),
1525 &[
1526 //
1527 "v root",
1528 " > a",
1529 " v b",
1530 " v a",
1531 " v inner_dir",
1532 " four.txt",
1533 " three.txt",
1534 " one.txt",
1535 " two.txt",
1536 " v c",
1537 " > a <== selected",
1538 " > inner_dir",
1539 " one.txt",
1540 " two.txt",
1541 ]
1542 );
1543}
1544
1545#[gpui::test]
1546async fn test_copy_paste_directory_with_sibling_file(cx: &mut gpui::TestAppContext) {
1547 init_test(cx);
1548
1549 let fs = FakeFs::new(cx.executor().clone());
1550 fs.insert_tree(
1551 "/test",
1552 json!({
1553 "dir1": {
1554 "a.txt": "",
1555 "b.txt": "",
1556 },
1557 "dir2": {},
1558 "c.txt": "",
1559 "d.txt": "",
1560 }),
1561 )
1562 .await;
1563
1564 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1565 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1566 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1567 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1568
1569 toggle_expand_dir(&panel, "test/dir1", cx);
1570
1571 cx.simulate_modifiers_change(gpui::Modifiers {
1572 control: true,
1573 ..Default::default()
1574 });
1575
1576 select_path_with_mark(&panel, "test/dir1", cx);
1577 select_path_with_mark(&panel, "test/c.txt", cx);
1578
1579 assert_eq!(
1580 visible_entries_as_strings(&panel, 0..15, cx),
1581 &[
1582 "v test",
1583 " v dir1 <== marked",
1584 " a.txt",
1585 " b.txt",
1586 " > dir2",
1587 " c.txt <== selected <== marked",
1588 " d.txt",
1589 ],
1590 "Initial state before copying dir1 and c.txt"
1591 );
1592
1593 panel.update_in(cx, |panel, window, cx| {
1594 panel.copy(&Default::default(), window, cx);
1595 });
1596 select_path(&panel, "test/dir2", cx);
1597 panel.update_in(cx, |panel, window, cx| {
1598 panel.paste(&Default::default(), window, cx);
1599 });
1600 cx.executor().run_until_parked();
1601
1602 toggle_expand_dir(&panel, "test/dir2/dir1", cx);
1603
1604 assert_eq!(
1605 visible_entries_as_strings(&panel, 0..15, cx),
1606 &[
1607 "v test",
1608 " v dir1 <== marked",
1609 " a.txt",
1610 " b.txt",
1611 " v dir2",
1612 " v dir1 <== selected",
1613 " a.txt",
1614 " b.txt",
1615 " c.txt",
1616 " c.txt <== marked",
1617 " d.txt",
1618 ],
1619 "Should copy dir1 as well as c.txt into dir2"
1620 );
1621
1622 // Disambiguating multiple files should not open the rename editor.
1623 select_path(&panel, "test/dir2", cx);
1624 panel.update_in(cx, |panel, window, cx| {
1625 panel.paste(&Default::default(), window, cx);
1626 });
1627 cx.executor().run_until_parked();
1628
1629 assert_eq!(
1630 visible_entries_as_strings(&panel, 0..15, cx),
1631 &[
1632 "v test",
1633 " v dir1 <== marked",
1634 " a.txt",
1635 " b.txt",
1636 " v dir2",
1637 " v dir1",
1638 " a.txt",
1639 " b.txt",
1640 " > dir1 copy <== selected",
1641 " c.txt",
1642 " c copy.txt",
1643 " c.txt <== marked",
1644 " d.txt",
1645 ],
1646 "Should copy dir1 as well as c.txt into dir2 and disambiguate them without opening the rename editor"
1647 );
1648}
1649
1650#[gpui::test]
1651async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) {
1652 init_test(cx);
1653
1654 let fs = FakeFs::new(cx.executor().clone());
1655 fs.insert_tree(
1656 "/test",
1657 json!({
1658 "dir1": {
1659 "a.txt": "",
1660 "b.txt": "",
1661 },
1662 "dir2": {},
1663 "c.txt": "",
1664 "d.txt": "",
1665 }),
1666 )
1667 .await;
1668
1669 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1670 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1671 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1672 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1673
1674 toggle_expand_dir(&panel, "test/dir1", cx);
1675
1676 cx.simulate_modifiers_change(gpui::Modifiers {
1677 control: true,
1678 ..Default::default()
1679 });
1680
1681 select_path_with_mark(&panel, "test/dir1/a.txt", cx);
1682 select_path_with_mark(&panel, "test/dir1", cx);
1683 select_path_with_mark(&panel, "test/c.txt", cx);
1684
1685 assert_eq!(
1686 visible_entries_as_strings(&panel, 0..15, cx),
1687 &[
1688 "v test",
1689 " v dir1 <== marked",
1690 " a.txt <== marked",
1691 " b.txt",
1692 " > dir2",
1693 " c.txt <== selected <== marked",
1694 " d.txt",
1695 ],
1696 "Initial state before copying a.txt, dir1 and c.txt"
1697 );
1698
1699 panel.update_in(cx, |panel, window, cx| {
1700 panel.copy(&Default::default(), window, cx);
1701 });
1702 select_path(&panel, "test/dir2", cx);
1703 panel.update_in(cx, |panel, window, cx| {
1704 panel.paste(&Default::default(), window, cx);
1705 });
1706 cx.executor().run_until_parked();
1707
1708 toggle_expand_dir(&panel, "test/dir2/dir1", cx);
1709
1710 assert_eq!(
1711 visible_entries_as_strings(&panel, 0..20, cx),
1712 &[
1713 "v test",
1714 " v dir1 <== marked",
1715 " a.txt <== marked",
1716 " b.txt",
1717 " v dir2",
1718 " v dir1 <== selected",
1719 " a.txt",
1720 " b.txt",
1721 " c.txt",
1722 " c.txt <== marked",
1723 " d.txt",
1724 ],
1725 "Should copy dir1 and c.txt into dir2. a.txt is already present in copied dir1."
1726 );
1727}
1728
1729#[gpui::test]
1730async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
1731 init_test_with_editor(cx);
1732
1733 let fs = FakeFs::new(cx.executor().clone());
1734 fs.insert_tree(
1735 path!("/src"),
1736 json!({
1737 "test": {
1738 "first.rs": "// First Rust file",
1739 "second.rs": "// Second Rust file",
1740 "third.rs": "// Third Rust file",
1741 }
1742 }),
1743 )
1744 .await;
1745
1746 let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await;
1747 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1748 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1749 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1750
1751 toggle_expand_dir(&panel, "src/test", cx);
1752 select_path(&panel, "src/test/first.rs", cx);
1753 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
1754 cx.executor().run_until_parked();
1755 assert_eq!(
1756 visible_entries_as_strings(&panel, 0..10, cx),
1757 &[
1758 "v src",
1759 " v test",
1760 " first.rs <== selected <== marked",
1761 " second.rs",
1762 " third.rs"
1763 ]
1764 );
1765 ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
1766
1767 submit_deletion(&panel, cx);
1768 assert_eq!(
1769 visible_entries_as_strings(&panel, 0..10, cx),
1770 &[
1771 "v src",
1772 " v test",
1773 " second.rs <== selected",
1774 " third.rs"
1775 ],
1776 "Project panel should have no deleted file, no other file is selected in it"
1777 );
1778 ensure_no_open_items_and_panes(&workspace, cx);
1779
1780 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
1781 cx.executor().run_until_parked();
1782 assert_eq!(
1783 visible_entries_as_strings(&panel, 0..10, cx),
1784 &[
1785 "v src",
1786 " v test",
1787 " second.rs <== selected <== marked",
1788 " third.rs"
1789 ]
1790 );
1791 ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
1792
1793 workspace
1794 .update(cx, |workspace, window, cx| {
1795 let active_items = workspace
1796 .panes()
1797 .iter()
1798 .filter_map(|pane| pane.read(cx).active_item())
1799 .collect::<Vec<_>>();
1800 assert_eq!(active_items.len(), 1);
1801 let open_editor = active_items
1802 .into_iter()
1803 .next()
1804 .unwrap()
1805 .downcast::<Editor>()
1806 .expect("Open item should be an editor");
1807 open_editor.update(cx, |editor, cx| {
1808 editor.set_text("Another text!", window, cx)
1809 });
1810 })
1811 .unwrap();
1812 submit_deletion_skipping_prompt(&panel, cx);
1813 assert_eq!(
1814 visible_entries_as_strings(&panel, 0..10, cx),
1815 &["v src", " v test", " third.rs <== selected"],
1816 "Project panel should have no deleted file, with one last file remaining"
1817 );
1818 ensure_no_open_items_and_panes(&workspace, cx);
1819}
1820
1821#[gpui::test]
1822async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
1823 init_test_with_editor(cx);
1824
1825 let fs = FakeFs::new(cx.executor().clone());
1826 fs.insert_tree(
1827 "/src",
1828 json!({
1829 "test": {
1830 "first.rs": "// First Rust file",
1831 "second.rs": "// Second Rust file",
1832 "third.rs": "// Third Rust file",
1833 }
1834 }),
1835 )
1836 .await;
1837
1838 let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
1839 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1840 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1841 let panel = workspace
1842 .update(cx, |workspace, window, cx| {
1843 let panel = ProjectPanel::new(workspace, window, cx);
1844 workspace.add_panel(panel.clone(), window, cx);
1845 panel
1846 })
1847 .unwrap();
1848
1849 select_path(&panel, "src/", cx);
1850 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
1851 cx.executor().run_until_parked();
1852 assert_eq!(
1853 visible_entries_as_strings(&panel, 0..10, cx),
1854 &[
1855 //
1856 "v src <== selected",
1857 " > test"
1858 ]
1859 );
1860 panel.update_in(cx, |panel, window, cx| {
1861 panel.new_directory(&NewDirectory, window, cx)
1862 });
1863 panel.update_in(cx, |panel, window, cx| {
1864 assert!(panel.filename_editor.read(cx).is_focused(window));
1865 });
1866 assert_eq!(
1867 visible_entries_as_strings(&panel, 0..10, cx),
1868 &[
1869 //
1870 "v src",
1871 " > [EDITOR: ''] <== selected",
1872 " > test"
1873 ]
1874 );
1875 panel.update_in(cx, |panel, window, cx| {
1876 panel
1877 .filename_editor
1878 .update(cx, |editor, cx| editor.set_text("test", window, cx));
1879 assert!(
1880 panel.confirm_edit(window, cx).is_none(),
1881 "Should not allow to confirm on conflicting new directory name"
1882 );
1883 });
1884 cx.executor().run_until_parked();
1885 panel.update_in(cx, |panel, window, cx| {
1886 assert!(
1887 panel.edit_state.is_some(),
1888 "Edit state should not be None after conflicting new directory name"
1889 );
1890 panel.cancel(&menu::Cancel, window, cx);
1891 });
1892 assert_eq!(
1893 visible_entries_as_strings(&panel, 0..10, cx),
1894 &[
1895 //
1896 "v src <== selected",
1897 " > test"
1898 ],
1899 "File list should be unchanged after failed folder create confirmation"
1900 );
1901
1902 select_path(&panel, "src/test/", cx);
1903 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
1904 cx.executor().run_until_parked();
1905 assert_eq!(
1906 visible_entries_as_strings(&panel, 0..10, cx),
1907 &[
1908 //
1909 "v src",
1910 " > test <== selected"
1911 ]
1912 );
1913 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
1914 panel.update_in(cx, |panel, window, cx| {
1915 assert!(panel.filename_editor.read(cx).is_focused(window));
1916 });
1917 assert_eq!(
1918 visible_entries_as_strings(&panel, 0..10, cx),
1919 &[
1920 "v src",
1921 " v test",
1922 " [EDITOR: ''] <== selected",
1923 " first.rs",
1924 " second.rs",
1925 " third.rs"
1926 ]
1927 );
1928 panel.update_in(cx, |panel, window, cx| {
1929 panel
1930 .filename_editor
1931 .update(cx, |editor, cx| editor.set_text("first.rs", window, cx));
1932 assert!(
1933 panel.confirm_edit(window, cx).is_none(),
1934 "Should not allow to confirm on conflicting new file name"
1935 );
1936 });
1937 cx.executor().run_until_parked();
1938 panel.update_in(cx, |panel, window, cx| {
1939 assert!(
1940 panel.edit_state.is_some(),
1941 "Edit state should not be None after conflicting new file name"
1942 );
1943 panel.cancel(&menu::Cancel, window, cx);
1944 });
1945 assert_eq!(
1946 visible_entries_as_strings(&panel, 0..10, cx),
1947 &[
1948 "v src",
1949 " v test <== selected",
1950 " first.rs",
1951 " second.rs",
1952 " third.rs"
1953 ],
1954 "File list should be unchanged after failed file create confirmation"
1955 );
1956
1957 select_path(&panel, "src/test/first.rs", cx);
1958 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
1959 cx.executor().run_until_parked();
1960 assert_eq!(
1961 visible_entries_as_strings(&panel, 0..10, cx),
1962 &[
1963 "v src",
1964 " v test",
1965 " first.rs <== selected",
1966 " second.rs",
1967 " third.rs"
1968 ],
1969 );
1970 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
1971 panel.update_in(cx, |panel, window, cx| {
1972 assert!(panel.filename_editor.read(cx).is_focused(window));
1973 });
1974 assert_eq!(
1975 visible_entries_as_strings(&panel, 0..10, cx),
1976 &[
1977 "v src",
1978 " v test",
1979 " [EDITOR: 'first.rs'] <== selected",
1980 " second.rs",
1981 " third.rs"
1982 ]
1983 );
1984 panel.update_in(cx, |panel, window, cx| {
1985 panel
1986 .filename_editor
1987 .update(cx, |editor, cx| editor.set_text("second.rs", window, cx));
1988 assert!(
1989 panel.confirm_edit(window, cx).is_none(),
1990 "Should not allow to confirm on conflicting file rename"
1991 )
1992 });
1993 cx.executor().run_until_parked();
1994 panel.update_in(cx, |panel, window, cx| {
1995 assert!(
1996 panel.edit_state.is_some(),
1997 "Edit state should not be None after conflicting file rename"
1998 );
1999 panel.cancel(&menu::Cancel, window, cx);
2000 });
2001 assert_eq!(
2002 visible_entries_as_strings(&panel, 0..10, cx),
2003 &[
2004 "v src",
2005 " v test",
2006 " first.rs <== selected",
2007 " second.rs",
2008 " third.rs"
2009 ],
2010 "File list should be unchanged after failed rename confirmation"
2011 );
2012}
2013
2014#[gpui::test]
2015async fn test_select_git_entry(cx: &mut gpui::TestAppContext) {
2016 init_test_with_editor(cx);
2017
2018 let fs = FakeFs::new(cx.executor().clone());
2019 fs.insert_tree(
2020 path!("/root"),
2021 json!({
2022 "tree1": {
2023 ".git": {},
2024 "dir1": {
2025 "modified1.txt": "1",
2026 "unmodified1.txt": "1",
2027 "modified2.txt": "1",
2028 },
2029 "dir2": {
2030 "modified3.txt": "1",
2031 "unmodified2.txt": "1",
2032 },
2033 "modified4.txt": "1",
2034 "unmodified3.txt": "1",
2035 },
2036 "tree2": {
2037 ".git": {},
2038 "dir3": {
2039 "modified5.txt": "1",
2040 "unmodified4.txt": "1",
2041 },
2042 "modified6.txt": "1",
2043 "unmodified5.txt": "1",
2044 }
2045 }),
2046 )
2047 .await;
2048
2049 // Mark files as git modified
2050 fs.set_git_content_for_repo(
2051 path!("/root/tree1/.git").as_ref(),
2052 &[
2053 ("dir1/modified1.txt".into(), "modified".into(), None),
2054 ("dir1/modified2.txt".into(), "modified".into(), None),
2055 ("modified4.txt".into(), "modified".into(), None),
2056 ("dir2/modified3.txt".into(), "modified".into(), None),
2057 ],
2058 );
2059 fs.set_git_content_for_repo(
2060 path!("/root/tree2/.git").as_ref(),
2061 &[
2062 ("dir3/modified5.txt".into(), "modified".into(), None),
2063 ("modified6.txt".into(), "modified".into(), None),
2064 ],
2065 );
2066
2067 let project = Project::test(
2068 fs.clone(),
2069 [path!("/root/tree1").as_ref(), path!("/root/tree2").as_ref()],
2070 cx,
2071 )
2072 .await;
2073
2074 let (scan1_complete, scan2_complete) = project.update(cx, |project, cx| {
2075 let mut worktrees = project.worktrees(cx);
2076 let worktree1 = worktrees.next().unwrap();
2077 let worktree2 = worktrees.next().unwrap();
2078 (
2079 worktree1.read(cx).as_local().unwrap().scan_complete(),
2080 worktree2.read(cx).as_local().unwrap().scan_complete(),
2081 )
2082 });
2083 scan1_complete.await;
2084 scan2_complete.await;
2085 cx.run_until_parked();
2086
2087 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2088 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2089 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2090
2091 // Check initial state
2092 assert_eq!(
2093 visible_entries_as_strings(&panel, 0..15, cx),
2094 &[
2095 "v tree1",
2096 " > .git",
2097 " > dir1",
2098 " > dir2",
2099 " modified4.txt",
2100 " unmodified3.txt",
2101 "v tree2",
2102 " > .git",
2103 " > dir3",
2104 " modified6.txt",
2105 " unmodified5.txt"
2106 ],
2107 );
2108
2109 // Test selecting next modified entry
2110 panel.update_in(cx, |panel, window, cx| {
2111 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2112 });
2113
2114 assert_eq!(
2115 visible_entries_as_strings(&panel, 0..6, cx),
2116 &[
2117 "v tree1",
2118 " > .git",
2119 " v dir1",
2120 " modified1.txt <== selected",
2121 " modified2.txt",
2122 " unmodified1.txt",
2123 ],
2124 );
2125
2126 panel.update_in(cx, |panel, window, cx| {
2127 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2128 });
2129
2130 assert_eq!(
2131 visible_entries_as_strings(&panel, 0..6, cx),
2132 &[
2133 "v tree1",
2134 " > .git",
2135 " v dir1",
2136 " modified1.txt",
2137 " modified2.txt <== selected",
2138 " unmodified1.txt",
2139 ],
2140 );
2141
2142 panel.update_in(cx, |panel, window, cx| {
2143 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2144 });
2145
2146 assert_eq!(
2147 visible_entries_as_strings(&panel, 6..9, cx),
2148 &[
2149 " v dir2",
2150 " modified3.txt <== selected",
2151 " unmodified2.txt",
2152 ],
2153 );
2154
2155 panel.update_in(cx, |panel, window, cx| {
2156 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2157 });
2158
2159 assert_eq!(
2160 visible_entries_as_strings(&panel, 9..11, cx),
2161 &[" modified4.txt <== selected", " unmodified3.txt",],
2162 );
2163
2164 panel.update_in(cx, |panel, window, cx| {
2165 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2166 });
2167
2168 assert_eq!(
2169 visible_entries_as_strings(&panel, 13..16, cx),
2170 &[
2171 " v dir3",
2172 " modified5.txt <== selected",
2173 " unmodified4.txt",
2174 ],
2175 );
2176
2177 panel.update_in(cx, |panel, window, cx| {
2178 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2179 });
2180
2181 assert_eq!(
2182 visible_entries_as_strings(&panel, 16..18, cx),
2183 &[" modified6.txt <== selected", " unmodified5.txt",],
2184 );
2185
2186 // Wraps around to first modified file
2187 panel.update_in(cx, |panel, window, cx| {
2188 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2189 });
2190
2191 assert_eq!(
2192 visible_entries_as_strings(&panel, 0..18, cx),
2193 &[
2194 "v tree1",
2195 " > .git",
2196 " v dir1",
2197 " modified1.txt <== selected",
2198 " modified2.txt",
2199 " unmodified1.txt",
2200 " v dir2",
2201 " modified3.txt",
2202 " unmodified2.txt",
2203 " modified4.txt",
2204 " unmodified3.txt",
2205 "v tree2",
2206 " > .git",
2207 " v dir3",
2208 " modified5.txt",
2209 " unmodified4.txt",
2210 " modified6.txt",
2211 " unmodified5.txt",
2212 ],
2213 );
2214
2215 // Wraps around again to last modified file
2216 panel.update_in(cx, |panel, window, cx| {
2217 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2218 });
2219
2220 assert_eq!(
2221 visible_entries_as_strings(&panel, 16..18, cx),
2222 &[" modified6.txt <== selected", " unmodified5.txt",],
2223 );
2224
2225 panel.update_in(cx, |panel, window, cx| {
2226 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2227 });
2228
2229 assert_eq!(
2230 visible_entries_as_strings(&panel, 13..16, cx),
2231 &[
2232 " v dir3",
2233 " modified5.txt <== selected",
2234 " unmodified4.txt",
2235 ],
2236 );
2237
2238 panel.update_in(cx, |panel, window, cx| {
2239 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2240 });
2241
2242 assert_eq!(
2243 visible_entries_as_strings(&panel, 9..11, cx),
2244 &[" modified4.txt <== selected", " unmodified3.txt",],
2245 );
2246
2247 panel.update_in(cx, |panel, window, cx| {
2248 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2249 });
2250
2251 assert_eq!(
2252 visible_entries_as_strings(&panel, 6..9, cx),
2253 &[
2254 " v dir2",
2255 " modified3.txt <== selected",
2256 " unmodified2.txt",
2257 ],
2258 );
2259
2260 panel.update_in(cx, |panel, window, cx| {
2261 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2262 });
2263
2264 assert_eq!(
2265 visible_entries_as_strings(&panel, 0..6, cx),
2266 &[
2267 "v tree1",
2268 " > .git",
2269 " v dir1",
2270 " modified1.txt",
2271 " modified2.txt <== selected",
2272 " unmodified1.txt",
2273 ],
2274 );
2275
2276 panel.update_in(cx, |panel, window, cx| {
2277 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2278 });
2279
2280 assert_eq!(
2281 visible_entries_as_strings(&panel, 0..6, cx),
2282 &[
2283 "v tree1",
2284 " > .git",
2285 " v dir1",
2286 " modified1.txt <== selected",
2287 " modified2.txt",
2288 " unmodified1.txt",
2289 ],
2290 );
2291}
2292
2293#[gpui::test]
2294async fn test_select_directory(cx: &mut gpui::TestAppContext) {
2295 init_test_with_editor(cx);
2296
2297 let fs = FakeFs::new(cx.executor().clone());
2298 fs.insert_tree(
2299 "/project_root",
2300 json!({
2301 "dir_1": {
2302 "nested_dir": {
2303 "file_a.py": "# File contents",
2304 }
2305 },
2306 "file_1.py": "# File contents",
2307 "dir_2": {
2308
2309 },
2310 "dir_3": {
2311
2312 },
2313 "file_2.py": "# File contents",
2314 "dir_4": {
2315
2316 },
2317 }),
2318 )
2319 .await;
2320
2321 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2322 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2323 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2324 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2325
2326 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2327 cx.executor().run_until_parked();
2328 select_path(&panel, "project_root/dir_1", cx);
2329 cx.executor().run_until_parked();
2330 assert_eq!(
2331 visible_entries_as_strings(&panel, 0..10, cx),
2332 &[
2333 "v project_root",
2334 " > dir_1 <== selected",
2335 " > dir_2",
2336 " > dir_3",
2337 " > dir_4",
2338 " file_1.py",
2339 " file_2.py",
2340 ]
2341 );
2342 panel.update_in(cx, |panel, window, cx| {
2343 panel.select_prev_directory(&SelectPrevDirectory, window, cx)
2344 });
2345
2346 assert_eq!(
2347 visible_entries_as_strings(&panel, 0..10, cx),
2348 &[
2349 "v project_root <== selected",
2350 " > dir_1",
2351 " > dir_2",
2352 " > dir_3",
2353 " > dir_4",
2354 " file_1.py",
2355 " file_2.py",
2356 ]
2357 );
2358
2359 panel.update_in(cx, |panel, window, cx| {
2360 panel.select_prev_directory(&SelectPrevDirectory, window, cx)
2361 });
2362
2363 assert_eq!(
2364 visible_entries_as_strings(&panel, 0..10, cx),
2365 &[
2366 "v project_root",
2367 " > dir_1",
2368 " > dir_2",
2369 " > dir_3",
2370 " > dir_4 <== selected",
2371 " file_1.py",
2372 " file_2.py",
2373 ]
2374 );
2375
2376 panel.update_in(cx, |panel, window, cx| {
2377 panel.select_next_directory(&SelectNextDirectory, window, cx)
2378 });
2379
2380 assert_eq!(
2381 visible_entries_as_strings(&panel, 0..10, cx),
2382 &[
2383 "v project_root <== selected",
2384 " > dir_1",
2385 " > dir_2",
2386 " > dir_3",
2387 " > dir_4",
2388 " file_1.py",
2389 " file_2.py",
2390 ]
2391 );
2392}
2393#[gpui::test]
2394async fn test_select_first_last(cx: &mut gpui::TestAppContext) {
2395 init_test_with_editor(cx);
2396
2397 let fs = FakeFs::new(cx.executor().clone());
2398 fs.insert_tree(
2399 "/project_root",
2400 json!({
2401 "dir_1": {
2402 "nested_dir": {
2403 "file_a.py": "# File contents",
2404 }
2405 },
2406 "file_1.py": "# File contents",
2407 "file_2.py": "# File contents",
2408 "zdir_2": {
2409 "nested_dir2": {
2410 "file_b.py": "# File contents",
2411 }
2412 },
2413 }),
2414 )
2415 .await;
2416
2417 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2418 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2419 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2420 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2421
2422 assert_eq!(
2423 visible_entries_as_strings(&panel, 0..10, cx),
2424 &[
2425 "v project_root",
2426 " > dir_1",
2427 " > zdir_2",
2428 " file_1.py",
2429 " file_2.py",
2430 ]
2431 );
2432 panel.update_in(cx, |panel, window, cx| {
2433 panel.select_first(&SelectFirst, window, cx)
2434 });
2435
2436 assert_eq!(
2437 visible_entries_as_strings(&panel, 0..10, cx),
2438 &[
2439 "v project_root <== selected",
2440 " > dir_1",
2441 " > zdir_2",
2442 " file_1.py",
2443 " file_2.py",
2444 ]
2445 );
2446
2447 panel.update_in(cx, |panel, window, cx| {
2448 panel.select_last(&SelectLast, window, cx)
2449 });
2450
2451 assert_eq!(
2452 visible_entries_as_strings(&panel, 0..10, cx),
2453 &[
2454 "v project_root",
2455 " > dir_1",
2456 " > zdir_2",
2457 " file_1.py",
2458 " file_2.py <== selected",
2459 ]
2460 );
2461}
2462
2463#[gpui::test]
2464async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) {
2465 init_test_with_editor(cx);
2466
2467 let fs = FakeFs::new(cx.executor().clone());
2468 fs.insert_tree(
2469 "/project_root",
2470 json!({
2471 "dir_1": {
2472 "nested_dir": {
2473 "file_a.py": "# File contents",
2474 }
2475 },
2476 "file_1.py": "# File contents",
2477 }),
2478 )
2479 .await;
2480
2481 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2482 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2483 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2484 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2485
2486 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2487 cx.executor().run_until_parked();
2488 select_path(&panel, "project_root/dir_1", cx);
2489 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2490 select_path(&panel, "project_root/dir_1/nested_dir", cx);
2491 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2492 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2493 cx.executor().run_until_parked();
2494 assert_eq!(
2495 visible_entries_as_strings(&panel, 0..10, cx),
2496 &[
2497 "v project_root",
2498 " v dir_1",
2499 " > nested_dir <== selected",
2500 " file_1.py",
2501 ]
2502 );
2503}
2504
2505#[gpui::test]
2506async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
2507 init_test_with_editor(cx);
2508
2509 let fs = FakeFs::new(cx.executor().clone());
2510 fs.insert_tree(
2511 "/project_root",
2512 json!({
2513 "dir_1": {
2514 "nested_dir": {
2515 "file_a.py": "# File contents",
2516 "file_b.py": "# File contents",
2517 "file_c.py": "# File contents",
2518 },
2519 "file_1.py": "# File contents",
2520 "file_2.py": "# File contents",
2521 "file_3.py": "# File contents",
2522 },
2523 "dir_2": {
2524 "file_1.py": "# File contents",
2525 "file_2.py": "# File contents",
2526 "file_3.py": "# File contents",
2527 }
2528 }),
2529 )
2530 .await;
2531
2532 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2533 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2534 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2535 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2536
2537 panel.update_in(cx, |panel, window, cx| {
2538 panel.collapse_all_entries(&CollapseAllEntries, window, cx)
2539 });
2540 cx.executor().run_until_parked();
2541 assert_eq!(
2542 visible_entries_as_strings(&panel, 0..10, cx),
2543 &["v project_root", " > dir_1", " > dir_2",]
2544 );
2545
2546 // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
2547 toggle_expand_dir(&panel, "project_root/dir_1", cx);
2548 cx.executor().run_until_parked();
2549 assert_eq!(
2550 visible_entries_as_strings(&panel, 0..10, cx),
2551 &[
2552 "v project_root",
2553 " v dir_1 <== selected",
2554 " > nested_dir",
2555 " file_1.py",
2556 " file_2.py",
2557 " file_3.py",
2558 " > dir_2",
2559 ]
2560 );
2561}
2562
2563#[gpui::test]
2564async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
2565 init_test(cx);
2566
2567 let fs = FakeFs::new(cx.executor().clone());
2568 fs.as_fake().insert_tree(path!("/root"), json!({})).await;
2569 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
2570 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2571 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2572 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2573
2574 // Make a new buffer with no backing file
2575 workspace
2576 .update(cx, |workspace, window, cx| {
2577 Editor::new_file(workspace, &Default::default(), window, cx)
2578 })
2579 .unwrap();
2580
2581 cx.executor().run_until_parked();
2582
2583 // "Save as" the buffer, creating a new backing file for it
2584 let save_task = workspace
2585 .update(cx, |workspace, window, cx| {
2586 workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
2587 })
2588 .unwrap();
2589
2590 cx.executor().run_until_parked();
2591 cx.simulate_new_path_selection(|_| Some(PathBuf::from(path!("/root/new"))));
2592 save_task.await.unwrap();
2593
2594 // Rename the file
2595 select_path(&panel, "root/new", cx);
2596 assert_eq!(
2597 visible_entries_as_strings(&panel, 0..10, cx),
2598 &["v root", " new <== selected <== marked"]
2599 );
2600 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2601 panel.update_in(cx, |panel, window, cx| {
2602 panel
2603 .filename_editor
2604 .update(cx, |editor, cx| editor.set_text("newer", window, cx));
2605 });
2606 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2607
2608 cx.executor().run_until_parked();
2609 assert_eq!(
2610 visible_entries_as_strings(&panel, 0..10, cx),
2611 &["v root", " newer <== selected"]
2612 );
2613
2614 workspace
2615 .update(cx, |workspace, window, cx| {
2616 workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
2617 })
2618 .unwrap()
2619 .await
2620 .unwrap();
2621
2622 cx.executor().run_until_parked();
2623 // assert that saving the file doesn't restore "new"
2624 assert_eq!(
2625 visible_entries_as_strings(&panel, 0..10, cx),
2626 &["v root", " newer <== selected"]
2627 );
2628}
2629
2630#[gpui::test]
2631#[cfg_attr(target_os = "windows", ignore)]
2632async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
2633 init_test_with_editor(cx);
2634
2635 let fs = FakeFs::new(cx.executor().clone());
2636 fs.insert_tree(
2637 "/root1",
2638 json!({
2639 "dir1": {
2640 "file1.txt": "content 1",
2641 },
2642 }),
2643 )
2644 .await;
2645
2646 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
2647 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2648 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2649 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2650
2651 toggle_expand_dir(&panel, "root1/dir1", cx);
2652
2653 assert_eq!(
2654 visible_entries_as_strings(&panel, 0..20, cx),
2655 &["v root1", " v dir1 <== selected", " file1.txt",],
2656 "Initial state with worktrees"
2657 );
2658
2659 select_path(&panel, "root1", cx);
2660 assert_eq!(
2661 visible_entries_as_strings(&panel, 0..20, cx),
2662 &["v root1 <== selected", " v dir1", " file1.txt",],
2663 );
2664
2665 // Rename root1 to new_root1
2666 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2667
2668 assert_eq!(
2669 visible_entries_as_strings(&panel, 0..20, cx),
2670 &[
2671 "v [EDITOR: 'root1'] <== selected",
2672 " v dir1",
2673 " file1.txt",
2674 ],
2675 );
2676
2677 let confirm = panel.update_in(cx, |panel, window, cx| {
2678 panel
2679 .filename_editor
2680 .update(cx, |editor, cx| editor.set_text("new_root1", window, cx));
2681 panel.confirm_edit(window, cx).unwrap()
2682 });
2683 confirm.await.unwrap();
2684 assert_eq!(
2685 visible_entries_as_strings(&panel, 0..20, cx),
2686 &[
2687 "v new_root1 <== selected",
2688 " v dir1",
2689 " file1.txt",
2690 ],
2691 "Should update worktree name"
2692 );
2693
2694 // Ensure internal paths have been updated
2695 select_path(&panel, "new_root1/dir1/file1.txt", cx);
2696 assert_eq!(
2697 visible_entries_as_strings(&panel, 0..20, cx),
2698 &[
2699 "v new_root1",
2700 " v dir1",
2701 " file1.txt <== selected",
2702 ],
2703 "Files in renamed worktree are selectable"
2704 );
2705}
2706
2707#[gpui::test]
2708async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
2709 init_test_with_editor(cx);
2710 let fs = FakeFs::new(cx.executor().clone());
2711 fs.insert_tree(
2712 "/project_root",
2713 json!({
2714 "dir_1": {
2715 "nested_dir": {
2716 "file_a.py": "# File contents",
2717 }
2718 },
2719 "file_1.py": "# File contents",
2720 }),
2721 )
2722 .await;
2723
2724 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2725 let worktree_id = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
2726 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2727 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2728 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2729 cx.update(|window, cx| {
2730 panel.update(cx, |this, cx| {
2731 this.select_next(&Default::default(), window, cx);
2732 this.expand_selected_entry(&Default::default(), window, cx);
2733 this.expand_selected_entry(&Default::default(), window, cx);
2734 this.select_next(&Default::default(), window, cx);
2735 this.expand_selected_entry(&Default::default(), window, cx);
2736 this.select_next(&Default::default(), window, cx);
2737 })
2738 });
2739 assert_eq!(
2740 visible_entries_as_strings(&panel, 0..10, cx),
2741 &[
2742 "v project_root",
2743 " v dir_1",
2744 " v nested_dir",
2745 " file_a.py <== selected",
2746 " file_1.py",
2747 ]
2748 );
2749 let modifiers_with_shift = gpui::Modifiers {
2750 shift: true,
2751 ..Default::default()
2752 };
2753 cx.run_until_parked();
2754 cx.simulate_modifiers_change(modifiers_with_shift);
2755 cx.update(|window, cx| {
2756 panel.update(cx, |this, cx| {
2757 this.select_next(&Default::default(), window, cx);
2758 })
2759 });
2760 assert_eq!(
2761 visible_entries_as_strings(&panel, 0..10, cx),
2762 &[
2763 "v project_root",
2764 " v dir_1",
2765 " v nested_dir",
2766 " file_a.py",
2767 " file_1.py <== selected <== marked",
2768 ]
2769 );
2770 cx.update(|window, cx| {
2771 panel.update(cx, |this, cx| {
2772 this.select_previous(&Default::default(), window, cx);
2773 })
2774 });
2775 assert_eq!(
2776 visible_entries_as_strings(&panel, 0..10, cx),
2777 &[
2778 "v project_root",
2779 " v dir_1",
2780 " v nested_dir",
2781 " file_a.py <== selected <== marked",
2782 " file_1.py <== marked",
2783 ]
2784 );
2785 cx.update(|window, cx| {
2786 panel.update(cx, |this, cx| {
2787 let drag = DraggedSelection {
2788 active_selection: this.selection.unwrap(),
2789 marked_selections: Arc::new(this.marked_entries.clone()),
2790 };
2791 let target_entry = this
2792 .project
2793 .read(cx)
2794 .entry_for_path(&(worktree_id, "").into(), cx)
2795 .unwrap();
2796 this.drag_onto(&drag, target_entry.id, false, window, cx);
2797 });
2798 });
2799 cx.run_until_parked();
2800 assert_eq!(
2801 visible_entries_as_strings(&panel, 0..10, cx),
2802 &[
2803 "v project_root",
2804 " v dir_1",
2805 " v nested_dir",
2806 " file_1.py <== marked",
2807 " file_a.py <== selected <== marked",
2808 ]
2809 );
2810 // ESC clears out all marks
2811 cx.update(|window, cx| {
2812 panel.update(cx, |this, cx| {
2813 this.cancel(&menu::Cancel, window, cx);
2814 })
2815 });
2816 assert_eq!(
2817 visible_entries_as_strings(&panel, 0..10, cx),
2818 &[
2819 "v project_root",
2820 " v dir_1",
2821 " v nested_dir",
2822 " file_1.py",
2823 " file_a.py <== selected",
2824 ]
2825 );
2826 // ESC clears out all marks
2827 cx.update(|window, cx| {
2828 panel.update(cx, |this, cx| {
2829 this.select_previous(&SelectPrevious, window, cx);
2830 this.select_next(&SelectNext, window, cx);
2831 })
2832 });
2833 assert_eq!(
2834 visible_entries_as_strings(&panel, 0..10, cx),
2835 &[
2836 "v project_root",
2837 " v dir_1",
2838 " v nested_dir",
2839 " file_1.py <== marked",
2840 " file_a.py <== selected <== marked",
2841 ]
2842 );
2843 cx.simulate_modifiers_change(Default::default());
2844 cx.update(|window, cx| {
2845 panel.update(cx, |this, cx| {
2846 this.cut(&Cut, window, cx);
2847 this.select_previous(&SelectPrevious, window, cx);
2848 this.select_previous(&SelectPrevious, window, cx);
2849
2850 this.paste(&Paste, window, cx);
2851 // this.expand_selected_entry(&ExpandSelectedEntry, cx);
2852 })
2853 });
2854 cx.run_until_parked();
2855 assert_eq!(
2856 visible_entries_as_strings(&panel, 0..10, cx),
2857 &[
2858 "v project_root",
2859 " v dir_1",
2860 " v nested_dir",
2861 " file_1.py <== marked",
2862 " file_a.py <== selected <== marked",
2863 ]
2864 );
2865 cx.simulate_modifiers_change(modifiers_with_shift);
2866 cx.update(|window, cx| {
2867 panel.update(cx, |this, cx| {
2868 this.expand_selected_entry(&Default::default(), window, cx);
2869 this.select_next(&SelectNext, window, cx);
2870 this.select_next(&SelectNext, window, cx);
2871 })
2872 });
2873 submit_deletion(&panel, cx);
2874 assert_eq!(
2875 visible_entries_as_strings(&panel, 0..10, cx),
2876 &[
2877 "v project_root",
2878 " v dir_1",
2879 " v nested_dir <== selected",
2880 ]
2881 );
2882}
2883#[gpui::test]
2884async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
2885 init_test_with_editor(cx);
2886 cx.update(|cx| {
2887 cx.update_global::<SettingsStore, _>(|store, cx| {
2888 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
2889 worktree_settings.file_scan_exclusions = Some(Vec::new());
2890 });
2891 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
2892 project_panel_settings.auto_reveal_entries = Some(false)
2893 });
2894 })
2895 });
2896
2897 let fs = FakeFs::new(cx.background_executor.clone());
2898 fs.insert_tree(
2899 "/project_root",
2900 json!({
2901 ".git": {},
2902 ".gitignore": "**/gitignored_dir",
2903 "dir_1": {
2904 "file_1.py": "# File 1_1 contents",
2905 "file_2.py": "# File 1_2 contents",
2906 "file_3.py": "# File 1_3 contents",
2907 "gitignored_dir": {
2908 "file_a.py": "# File contents",
2909 "file_b.py": "# File contents",
2910 "file_c.py": "# File contents",
2911 },
2912 },
2913 "dir_2": {
2914 "file_1.py": "# File 2_1 contents",
2915 "file_2.py": "# File 2_2 contents",
2916 "file_3.py": "# File 2_3 contents",
2917 }
2918 }),
2919 )
2920 .await;
2921
2922 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2923 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2924 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2925 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2926
2927 assert_eq!(
2928 visible_entries_as_strings(&panel, 0..20, cx),
2929 &[
2930 "v project_root",
2931 " > .git",
2932 " > dir_1",
2933 " > dir_2",
2934 " .gitignore",
2935 ]
2936 );
2937
2938 let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
2939 .expect("dir 1 file is not ignored and should have an entry");
2940 let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
2941 .expect("dir 2 file is not ignored and should have an entry");
2942 let gitignored_dir_file =
2943 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
2944 assert_eq!(
2945 gitignored_dir_file, None,
2946 "File in the gitignored dir should not have an entry before its dir is toggled"
2947 );
2948
2949 toggle_expand_dir(&panel, "project_root/dir_1", cx);
2950 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
2951 cx.executor().run_until_parked();
2952 assert_eq!(
2953 visible_entries_as_strings(&panel, 0..20, cx),
2954 &[
2955 "v project_root",
2956 " > .git",
2957 " v dir_1",
2958 " v gitignored_dir <== selected",
2959 " file_a.py",
2960 " file_b.py",
2961 " file_c.py",
2962 " file_1.py",
2963 " file_2.py",
2964 " file_3.py",
2965 " > dir_2",
2966 " .gitignore",
2967 ],
2968 "Should show gitignored dir file list in the project panel"
2969 );
2970 let gitignored_dir_file =
2971 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
2972 .expect("after gitignored dir got opened, a file entry should be present");
2973
2974 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
2975 toggle_expand_dir(&panel, "project_root/dir_1", cx);
2976 assert_eq!(
2977 visible_entries_as_strings(&panel, 0..20, cx),
2978 &[
2979 "v project_root",
2980 " > .git",
2981 " > dir_1 <== selected",
2982 " > dir_2",
2983 " .gitignore",
2984 ],
2985 "Should hide all dir contents again and prepare for the auto reveal test"
2986 );
2987
2988 for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
2989 panel.update(cx, |panel, cx| {
2990 panel.project.update(cx, |_, cx| {
2991 cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
2992 })
2993 });
2994 cx.run_until_parked();
2995 assert_eq!(
2996 visible_entries_as_strings(&panel, 0..20, cx),
2997 &[
2998 "v project_root",
2999 " > .git",
3000 " > dir_1 <== selected",
3001 " > dir_2",
3002 " .gitignore",
3003 ],
3004 "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
3005 );
3006 }
3007
3008 cx.update(|_, cx| {
3009 cx.update_global::<SettingsStore, _>(|store, cx| {
3010 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3011 project_panel_settings.auto_reveal_entries = Some(true)
3012 });
3013 })
3014 });
3015
3016 panel.update(cx, |panel, cx| {
3017 panel.project.update(cx, |_, cx| {
3018 cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
3019 })
3020 });
3021 cx.run_until_parked();
3022 assert_eq!(
3023 visible_entries_as_strings(&panel, 0..20, cx),
3024 &[
3025 "v project_root",
3026 " > .git",
3027 " v dir_1",
3028 " > gitignored_dir",
3029 " file_1.py <== selected <== marked",
3030 " file_2.py",
3031 " file_3.py",
3032 " > dir_2",
3033 " .gitignore",
3034 ],
3035 "When auto reveal is enabled, not ignored dir_1 entry should be revealed"
3036 );
3037
3038 panel.update(cx, |panel, cx| {
3039 panel.project.update(cx, |_, cx| {
3040 cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
3041 })
3042 });
3043 cx.run_until_parked();
3044 assert_eq!(
3045 visible_entries_as_strings(&panel, 0..20, cx),
3046 &[
3047 "v project_root",
3048 " > .git",
3049 " v dir_1",
3050 " > gitignored_dir",
3051 " file_1.py",
3052 " file_2.py",
3053 " file_3.py",
3054 " v dir_2",
3055 " file_1.py <== selected <== marked",
3056 " file_2.py",
3057 " file_3.py",
3058 " .gitignore",
3059 ],
3060 "When auto reveal is enabled, not ignored dir_2 entry should be revealed"
3061 );
3062
3063 panel.update(cx, |panel, cx| {
3064 panel.project.update(cx, |_, cx| {
3065 cx.emit(project::Event::ActiveEntryChanged(Some(
3066 gitignored_dir_file,
3067 )))
3068 })
3069 });
3070 cx.run_until_parked();
3071 assert_eq!(
3072 visible_entries_as_strings(&panel, 0..20, cx),
3073 &[
3074 "v project_root",
3075 " > .git",
3076 " v dir_1",
3077 " > gitignored_dir",
3078 " file_1.py",
3079 " file_2.py",
3080 " file_3.py",
3081 " v dir_2",
3082 " file_1.py <== selected <== marked",
3083 " file_2.py",
3084 " file_3.py",
3085 " .gitignore",
3086 ],
3087 "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
3088 );
3089
3090 panel.update(cx, |panel, cx| {
3091 panel.project.update(cx, |_, cx| {
3092 cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
3093 })
3094 });
3095 cx.run_until_parked();
3096 assert_eq!(
3097 visible_entries_as_strings(&panel, 0..20, cx),
3098 &[
3099 "v project_root",
3100 " > .git",
3101 " v dir_1",
3102 " v gitignored_dir",
3103 " file_a.py <== selected <== marked",
3104 " file_b.py",
3105 " file_c.py",
3106 " file_1.py",
3107 " file_2.py",
3108 " file_3.py",
3109 " v dir_2",
3110 " file_1.py",
3111 " file_2.py",
3112 " file_3.py",
3113 " .gitignore",
3114 ],
3115 "When a gitignored entry is explicitly revealed, it should be shown in the project tree"
3116 );
3117}
3118
3119#[gpui::test]
3120async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
3121 init_test_with_editor(cx);
3122 cx.update(|cx| {
3123 cx.update_global::<SettingsStore, _>(|store, cx| {
3124 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3125 worktree_settings.file_scan_exclusions = Some(Vec::new());
3126 worktree_settings.file_scan_inclusions =
3127 Some(vec!["always_included_but_ignored_dir/*".to_string()]);
3128 });
3129 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3130 project_panel_settings.auto_reveal_entries = Some(false)
3131 });
3132 })
3133 });
3134
3135 let fs = FakeFs::new(cx.background_executor.clone());
3136 fs.insert_tree(
3137 "/project_root",
3138 json!({
3139 ".git": {},
3140 ".gitignore": "**/gitignored_dir\n/always_included_but_ignored_dir",
3141 "dir_1": {
3142 "file_1.py": "# File 1_1 contents",
3143 "file_2.py": "# File 1_2 contents",
3144 "file_3.py": "# File 1_3 contents",
3145 "gitignored_dir": {
3146 "file_a.py": "# File contents",
3147 "file_b.py": "# File contents",
3148 "file_c.py": "# File contents",
3149 },
3150 },
3151 "dir_2": {
3152 "file_1.py": "# File 2_1 contents",
3153 "file_2.py": "# File 2_2 contents",
3154 "file_3.py": "# File 2_3 contents",
3155 },
3156 "always_included_but_ignored_dir": {
3157 "file_a.py": "# File contents",
3158 "file_b.py": "# File contents",
3159 "file_c.py": "# File contents",
3160 },
3161 }),
3162 )
3163 .await;
3164
3165 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3166 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3167 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3168 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3169
3170 assert_eq!(
3171 visible_entries_as_strings(&panel, 0..20, cx),
3172 &[
3173 "v project_root",
3174 " > .git",
3175 " > always_included_but_ignored_dir",
3176 " > dir_1",
3177 " > dir_2",
3178 " .gitignore",
3179 ]
3180 );
3181
3182 let gitignored_dir_file =
3183 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3184 let always_included_but_ignored_dir_file = find_project_entry(
3185 &panel,
3186 "project_root/always_included_but_ignored_dir/file_a.py",
3187 cx,
3188 )
3189 .expect("file that is .gitignored but set to always be included should have an entry");
3190 assert_eq!(
3191 gitignored_dir_file, None,
3192 "File in the gitignored dir should not have an entry unless its directory is toggled"
3193 );
3194
3195 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3196 cx.run_until_parked();
3197 cx.update(|_, cx| {
3198 cx.update_global::<SettingsStore, _>(|store, cx| {
3199 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3200 project_panel_settings.auto_reveal_entries = Some(true)
3201 });
3202 })
3203 });
3204
3205 panel.update(cx, |panel, cx| {
3206 panel.project.update(cx, |_, cx| {
3207 cx.emit(project::Event::ActiveEntryChanged(Some(
3208 always_included_but_ignored_dir_file,
3209 )))
3210 })
3211 });
3212 cx.run_until_parked();
3213
3214 assert_eq!(
3215 visible_entries_as_strings(&panel, 0..20, cx),
3216 &[
3217 "v project_root",
3218 " > .git",
3219 " v always_included_but_ignored_dir",
3220 " file_a.py <== selected <== marked",
3221 " file_b.py",
3222 " file_c.py",
3223 " v dir_1",
3224 " > gitignored_dir",
3225 " file_1.py",
3226 " file_2.py",
3227 " file_3.py",
3228 " > dir_2",
3229 " .gitignore",
3230 ],
3231 "When auto reveal is enabled, a gitignored but always included selected entry should be revealed in the project panel"
3232 );
3233}
3234
3235#[gpui::test]
3236async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
3237 init_test_with_editor(cx);
3238 cx.update(|cx| {
3239 cx.update_global::<SettingsStore, _>(|store, cx| {
3240 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3241 worktree_settings.file_scan_exclusions = Some(Vec::new());
3242 });
3243 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3244 project_panel_settings.auto_reveal_entries = Some(false)
3245 });
3246 })
3247 });
3248
3249 let fs = FakeFs::new(cx.background_executor.clone());
3250 fs.insert_tree(
3251 "/project_root",
3252 json!({
3253 ".git": {},
3254 ".gitignore": "**/gitignored_dir",
3255 "dir_1": {
3256 "file_1.py": "# File 1_1 contents",
3257 "file_2.py": "# File 1_2 contents",
3258 "file_3.py": "# File 1_3 contents",
3259 "gitignored_dir": {
3260 "file_a.py": "# File contents",
3261 "file_b.py": "# File contents",
3262 "file_c.py": "# File contents",
3263 },
3264 },
3265 "dir_2": {
3266 "file_1.py": "# File 2_1 contents",
3267 "file_2.py": "# File 2_2 contents",
3268 "file_3.py": "# File 2_3 contents",
3269 }
3270 }),
3271 )
3272 .await;
3273
3274 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3275 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3276 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3277 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3278
3279 assert_eq!(
3280 visible_entries_as_strings(&panel, 0..20, cx),
3281 &[
3282 "v project_root",
3283 " > .git",
3284 " > dir_1",
3285 " > dir_2",
3286 " .gitignore",
3287 ]
3288 );
3289
3290 let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
3291 .expect("dir 1 file is not ignored and should have an entry");
3292 let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
3293 .expect("dir 2 file is not ignored and should have an entry");
3294 let gitignored_dir_file =
3295 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3296 assert_eq!(
3297 gitignored_dir_file, None,
3298 "File in the gitignored dir should not have an entry before its dir is toggled"
3299 );
3300
3301 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3302 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3303 cx.run_until_parked();
3304 assert_eq!(
3305 visible_entries_as_strings(&panel, 0..20, cx),
3306 &[
3307 "v project_root",
3308 " > .git",
3309 " v dir_1",
3310 " v gitignored_dir <== selected",
3311 " file_a.py",
3312 " file_b.py",
3313 " file_c.py",
3314 " file_1.py",
3315 " file_2.py",
3316 " file_3.py",
3317 " > dir_2",
3318 " .gitignore",
3319 ],
3320 "Should show gitignored dir file list in the project panel"
3321 );
3322 let gitignored_dir_file =
3323 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
3324 .expect("after gitignored dir got opened, a file entry should be present");
3325
3326 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3327 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3328 assert_eq!(
3329 visible_entries_as_strings(&panel, 0..20, cx),
3330 &[
3331 "v project_root",
3332 " > .git",
3333 " > dir_1 <== selected",
3334 " > dir_2",
3335 " .gitignore",
3336 ],
3337 "Should hide all dir contents again and prepare for the explicit reveal test"
3338 );
3339
3340 for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
3341 panel.update(cx, |panel, cx| {
3342 panel.project.update(cx, |_, cx| {
3343 cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
3344 })
3345 });
3346 cx.run_until_parked();
3347 assert_eq!(
3348 visible_entries_as_strings(&panel, 0..20, cx),
3349 &[
3350 "v project_root",
3351 " > .git",
3352 " > dir_1 <== selected",
3353 " > dir_2",
3354 " .gitignore",
3355 ],
3356 "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
3357 );
3358 }
3359
3360 panel.update(cx, |panel, cx| {
3361 panel.project.update(cx, |_, cx| {
3362 cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
3363 })
3364 });
3365 cx.run_until_parked();
3366 assert_eq!(
3367 visible_entries_as_strings(&panel, 0..20, cx),
3368 &[
3369 "v project_root",
3370 " > .git",
3371 " v dir_1",
3372 " > gitignored_dir",
3373 " file_1.py <== selected <== marked",
3374 " file_2.py",
3375 " file_3.py",
3376 " > dir_2",
3377 " .gitignore",
3378 ],
3379 "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
3380 );
3381
3382 panel.update(cx, |panel, cx| {
3383 panel.project.update(cx, |_, cx| {
3384 cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
3385 })
3386 });
3387 cx.run_until_parked();
3388 assert_eq!(
3389 visible_entries_as_strings(&panel, 0..20, cx),
3390 &[
3391 "v project_root",
3392 " > .git",
3393 " v dir_1",
3394 " > gitignored_dir",
3395 " file_1.py",
3396 " file_2.py",
3397 " file_3.py",
3398 " v dir_2",
3399 " file_1.py <== selected <== marked",
3400 " file_2.py",
3401 " file_3.py",
3402 " .gitignore",
3403 ],
3404 "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
3405 );
3406
3407 panel.update(cx, |panel, cx| {
3408 panel.project.update(cx, |_, cx| {
3409 cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
3410 })
3411 });
3412 cx.run_until_parked();
3413 assert_eq!(
3414 visible_entries_as_strings(&panel, 0..20, cx),
3415 &[
3416 "v project_root",
3417 " > .git",
3418 " v dir_1",
3419 " v gitignored_dir",
3420 " file_a.py <== selected <== marked",
3421 " file_b.py",
3422 " file_c.py",
3423 " file_1.py",
3424 " file_2.py",
3425 " file_3.py",
3426 " v dir_2",
3427 " file_1.py",
3428 " file_2.py",
3429 " file_3.py",
3430 " .gitignore",
3431 ],
3432 "With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
3433 );
3434}
3435
3436#[gpui::test]
3437async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
3438 init_test(cx);
3439 cx.update(|cx| {
3440 cx.update_global::<SettingsStore, _>(|store, cx| {
3441 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
3442 project_settings.file_scan_exclusions =
3443 Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
3444 });
3445 });
3446 });
3447
3448 cx.update(|cx| {
3449 register_project_item::<TestProjectItemView>(cx);
3450 });
3451
3452 let fs = FakeFs::new(cx.executor().clone());
3453 fs.insert_tree(
3454 "/root1",
3455 json!({
3456 ".dockerignore": "",
3457 ".git": {
3458 "HEAD": "",
3459 },
3460 }),
3461 )
3462 .await;
3463
3464 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
3465 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3466 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3467 let panel = workspace
3468 .update(cx, |workspace, window, cx| {
3469 let panel = ProjectPanel::new(workspace, window, cx);
3470 workspace.add_panel(panel.clone(), window, cx);
3471 panel
3472 })
3473 .unwrap();
3474
3475 select_path(&panel, "root1", cx);
3476 assert_eq!(
3477 visible_entries_as_strings(&panel, 0..10, cx),
3478 &["v root1 <== selected", " .dockerignore",]
3479 );
3480 workspace
3481 .update(cx, |workspace, _, cx| {
3482 assert!(
3483 workspace.active_item(cx).is_none(),
3484 "Should have no active items in the beginning"
3485 );
3486 })
3487 .unwrap();
3488
3489 let excluded_file_path = ".git/COMMIT_EDITMSG";
3490 let excluded_dir_path = "excluded_dir";
3491
3492 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
3493 panel.update_in(cx, |panel, window, cx| {
3494 assert!(panel.filename_editor.read(cx).is_focused(window));
3495 });
3496 panel
3497 .update_in(cx, |panel, window, cx| {
3498 panel.filename_editor.update(cx, |editor, cx| {
3499 editor.set_text(excluded_file_path, window, cx)
3500 });
3501 panel.confirm_edit(window, cx).unwrap()
3502 })
3503 .await
3504 .unwrap();
3505
3506 assert_eq!(
3507 visible_entries_as_strings(&panel, 0..13, cx),
3508 &["v root1", " .dockerignore"],
3509 "Excluded dir should not be shown after opening a file in it"
3510 );
3511 panel.update_in(cx, |panel, window, cx| {
3512 assert!(
3513 !panel.filename_editor.read(cx).is_focused(window),
3514 "Should have closed the file name editor"
3515 );
3516 });
3517 workspace
3518 .update(cx, |workspace, _, cx| {
3519 let active_entry_path = workspace
3520 .active_item(cx)
3521 .expect("should have opened and activated the excluded item")
3522 .act_as::<TestProjectItemView>(cx)
3523 .expect("should have opened the corresponding project item for the excluded item")
3524 .read(cx)
3525 .path
3526 .clone();
3527 assert_eq!(
3528 active_entry_path.path.as_ref(),
3529 Path::new(excluded_file_path),
3530 "Should open the excluded file"
3531 );
3532
3533 assert!(
3534 workspace.notification_ids().is_empty(),
3535 "Should have no notifications after opening an excluded file"
3536 );
3537 })
3538 .unwrap();
3539 assert!(
3540 fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
3541 "Should have created the excluded file"
3542 );
3543
3544 select_path(&panel, "root1", cx);
3545 panel.update_in(cx, |panel, window, cx| {
3546 panel.new_directory(&NewDirectory, window, cx)
3547 });
3548 panel.update_in(cx, |panel, window, cx| {
3549 assert!(panel.filename_editor.read(cx).is_focused(window));
3550 });
3551 panel
3552 .update_in(cx, |panel, window, cx| {
3553 panel.filename_editor.update(cx, |editor, cx| {
3554 editor.set_text(excluded_file_path, window, cx)
3555 });
3556 panel.confirm_edit(window, cx).unwrap()
3557 })
3558 .await
3559 .unwrap();
3560
3561 assert_eq!(
3562 visible_entries_as_strings(&panel, 0..13, cx),
3563 &["v root1", " .dockerignore"],
3564 "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
3565 );
3566 panel.update_in(cx, |panel, window, cx| {
3567 assert!(
3568 !panel.filename_editor.read(cx).is_focused(window),
3569 "Should have closed the file name editor"
3570 );
3571 });
3572 workspace
3573 .update(cx, |workspace, _, cx| {
3574 let notifications = workspace.notification_ids();
3575 assert_eq!(
3576 notifications.len(),
3577 1,
3578 "Should receive one notification with the error message"
3579 );
3580 workspace.dismiss_notification(notifications.first().unwrap(), cx);
3581 assert!(workspace.notification_ids().is_empty());
3582 })
3583 .unwrap();
3584
3585 select_path(&panel, "root1", cx);
3586 panel.update_in(cx, |panel, window, cx| {
3587 panel.new_directory(&NewDirectory, window, cx)
3588 });
3589 panel.update_in(cx, |panel, window, cx| {
3590 assert!(panel.filename_editor.read(cx).is_focused(window));
3591 });
3592 panel
3593 .update_in(cx, |panel, window, cx| {
3594 panel.filename_editor.update(cx, |editor, cx| {
3595 editor.set_text(excluded_dir_path, window, cx)
3596 });
3597 panel.confirm_edit(window, cx).unwrap()
3598 })
3599 .await
3600 .unwrap();
3601
3602 assert_eq!(
3603 visible_entries_as_strings(&panel, 0..13, cx),
3604 &["v root1", " .dockerignore"],
3605 "Should not change the project panel after trying to create an excluded directory"
3606 );
3607 panel.update_in(cx, |panel, window, cx| {
3608 assert!(
3609 !panel.filename_editor.read(cx).is_focused(window),
3610 "Should have closed the file name editor"
3611 );
3612 });
3613 workspace
3614 .update(cx, |workspace, _, cx| {
3615 let notifications = workspace.notification_ids();
3616 assert_eq!(
3617 notifications.len(),
3618 1,
3619 "Should receive one notification explaining that no directory is actually shown"
3620 );
3621 workspace.dismiss_notification(notifications.first().unwrap(), cx);
3622 assert!(workspace.notification_ids().is_empty());
3623 })
3624 .unwrap();
3625 assert!(
3626 fs.is_dir(Path::new("/root1/excluded_dir")).await,
3627 "Should have created the excluded directory"
3628 );
3629}
3630
3631#[gpui::test]
3632async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) {
3633 init_test_with_editor(cx);
3634
3635 let fs = FakeFs::new(cx.executor().clone());
3636 fs.insert_tree(
3637 "/src",
3638 json!({
3639 "test": {
3640 "first.rs": "// First Rust file",
3641 "second.rs": "// Second Rust file",
3642 "third.rs": "// Third Rust file",
3643 }
3644 }),
3645 )
3646 .await;
3647
3648 let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
3649 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3650 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3651 let panel = workspace
3652 .update(cx, |workspace, window, cx| {
3653 let panel = ProjectPanel::new(workspace, window, cx);
3654 workspace.add_panel(panel.clone(), window, cx);
3655 panel
3656 })
3657 .unwrap();
3658
3659 select_path(&panel, "src/", cx);
3660 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
3661 cx.executor().run_until_parked();
3662 assert_eq!(
3663 visible_entries_as_strings(&panel, 0..10, cx),
3664 &[
3665 //
3666 "v src <== selected",
3667 " > test"
3668 ]
3669 );
3670 panel.update_in(cx, |panel, window, cx| {
3671 panel.new_directory(&NewDirectory, window, cx)
3672 });
3673 panel.update_in(cx, |panel, window, cx| {
3674 assert!(panel.filename_editor.read(cx).is_focused(window));
3675 });
3676 assert_eq!(
3677 visible_entries_as_strings(&panel, 0..10, cx),
3678 &[
3679 //
3680 "v src",
3681 " > [EDITOR: ''] <== selected",
3682 " > test"
3683 ]
3684 );
3685
3686 panel.update_in(cx, |panel, window, cx| {
3687 panel.cancel(&menu::Cancel, window, cx)
3688 });
3689 assert_eq!(
3690 visible_entries_as_strings(&panel, 0..10, cx),
3691 &[
3692 //
3693 "v src <== selected",
3694 " > test"
3695 ]
3696 );
3697}
3698
3699#[gpui::test]
3700async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
3701 init_test_with_editor(cx);
3702
3703 let fs = FakeFs::new(cx.executor().clone());
3704 fs.insert_tree(
3705 "/root",
3706 json!({
3707 "dir1": {
3708 "subdir1": {},
3709 "file1.txt": "",
3710 "file2.txt": "",
3711 },
3712 "dir2": {
3713 "subdir2": {},
3714 "file3.txt": "",
3715 "file4.txt": "",
3716 },
3717 "file5.txt": "",
3718 "file6.txt": "",
3719 }),
3720 )
3721 .await;
3722
3723 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
3724 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3725 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3726 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3727
3728 toggle_expand_dir(&panel, "root/dir1", cx);
3729 toggle_expand_dir(&panel, "root/dir2", cx);
3730
3731 // Test Case 1: Delete middle file in directory
3732 select_path(&panel, "root/dir1/file1.txt", cx);
3733 assert_eq!(
3734 visible_entries_as_strings(&panel, 0..15, cx),
3735 &[
3736 "v root",
3737 " v dir1",
3738 " > subdir1",
3739 " file1.txt <== selected",
3740 " file2.txt",
3741 " v dir2",
3742 " > subdir2",
3743 " file3.txt",
3744 " file4.txt",
3745 " file5.txt",
3746 " file6.txt",
3747 ],
3748 "Initial state before deleting middle file"
3749 );
3750
3751 submit_deletion(&panel, cx);
3752 assert_eq!(
3753 visible_entries_as_strings(&panel, 0..15, cx),
3754 &[
3755 "v root",
3756 " v dir1",
3757 " > subdir1",
3758 " file2.txt <== selected",
3759 " v dir2",
3760 " > subdir2",
3761 " file3.txt",
3762 " file4.txt",
3763 " file5.txt",
3764 " file6.txt",
3765 ],
3766 "Should select next file after deleting middle file"
3767 );
3768
3769 // Test Case 2: Delete last file in directory
3770 submit_deletion(&panel, cx);
3771 assert_eq!(
3772 visible_entries_as_strings(&panel, 0..15, cx),
3773 &[
3774 "v root",
3775 " v dir1",
3776 " > subdir1 <== selected",
3777 " v dir2",
3778 " > subdir2",
3779 " file3.txt",
3780 " file4.txt",
3781 " file5.txt",
3782 " file6.txt",
3783 ],
3784 "Should select next directory when last file is deleted"
3785 );
3786
3787 // Test Case 3: Delete root level file
3788 select_path(&panel, "root/file6.txt", cx);
3789 assert_eq!(
3790 visible_entries_as_strings(&panel, 0..15, cx),
3791 &[
3792 "v root",
3793 " v dir1",
3794 " > subdir1",
3795 " v dir2",
3796 " > subdir2",
3797 " file3.txt",
3798 " file4.txt",
3799 " file5.txt",
3800 " file6.txt <== selected",
3801 ],
3802 "Initial state before deleting root level file"
3803 );
3804
3805 submit_deletion(&panel, cx);
3806 assert_eq!(
3807 visible_entries_as_strings(&panel, 0..15, cx),
3808 &[
3809 "v root",
3810 " v dir1",
3811 " > subdir1",
3812 " v dir2",
3813 " > subdir2",
3814 " file3.txt",
3815 " file4.txt",
3816 " file5.txt <== selected",
3817 ],
3818 "Should select prev entry at root level"
3819 );
3820}
3821
3822#[gpui::test]
3823async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) {
3824 init_test_with_editor(cx);
3825
3826 let fs = FakeFs::new(cx.executor().clone());
3827 fs.insert_tree(
3828 path!("/root"),
3829 json!({
3830 "aa": "// Testing 1",
3831 "bb": "// Testing 2",
3832 "cc": "// Testing 3",
3833 "dd": "// Testing 4",
3834 "ee": "// Testing 5",
3835 "ff": "// Testing 6",
3836 "gg": "// Testing 7",
3837 "hh": "// Testing 8",
3838 "ii": "// Testing 8",
3839 ".gitignore": "bb\ndd\nee\nff\nii\n'",
3840 }),
3841 )
3842 .await;
3843
3844 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3845 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3846 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3847
3848 // Test 1: Auto selection with one gitignored file next to the deleted file
3849 cx.update(|_, cx| {
3850 let settings = *ProjectPanelSettings::get_global(cx);
3851 ProjectPanelSettings::override_global(
3852 ProjectPanelSettings {
3853 hide_gitignore: true,
3854 ..settings
3855 },
3856 cx,
3857 );
3858 });
3859
3860 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3861
3862 select_path(&panel, "root/aa", cx);
3863 assert_eq!(
3864 visible_entries_as_strings(&panel, 0..10, cx),
3865 &[
3866 "v root",
3867 " .gitignore",
3868 " aa <== selected",
3869 " cc",
3870 " gg",
3871 " hh"
3872 ],
3873 "Initial state should hide files on .gitignore"
3874 );
3875
3876 submit_deletion(&panel, cx);
3877
3878 assert_eq!(
3879 visible_entries_as_strings(&panel, 0..10, cx),
3880 &[
3881 "v root",
3882 " .gitignore",
3883 " cc <== selected",
3884 " gg",
3885 " hh"
3886 ],
3887 "Should select next entry not on .gitignore"
3888 );
3889
3890 // Test 2: Auto selection with many gitignored files next to the deleted file
3891 submit_deletion(&panel, cx);
3892 assert_eq!(
3893 visible_entries_as_strings(&panel, 0..10, cx),
3894 &[
3895 "v root",
3896 " .gitignore",
3897 " gg <== selected",
3898 " hh"
3899 ],
3900 "Should select next entry not on .gitignore"
3901 );
3902
3903 // Test 3: Auto selection of entry before deleted file
3904 select_path(&panel, "root/hh", cx);
3905 assert_eq!(
3906 visible_entries_as_strings(&panel, 0..10, cx),
3907 &[
3908 "v root",
3909 " .gitignore",
3910 " gg",
3911 " hh <== selected"
3912 ],
3913 "Should select next entry not on .gitignore"
3914 );
3915 submit_deletion(&panel, cx);
3916 assert_eq!(
3917 visible_entries_as_strings(&panel, 0..10, cx),
3918 &["v root", " .gitignore", " gg <== selected"],
3919 "Should select next entry not on .gitignore"
3920 );
3921}
3922
3923#[gpui::test]
3924async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) {
3925 init_test_with_editor(cx);
3926
3927 let fs = FakeFs::new(cx.executor().clone());
3928 fs.insert_tree(
3929 path!("/root"),
3930 json!({
3931 "dir1": {
3932 "file1": "// Testing",
3933 "file2": "// Testing",
3934 "file3": "// Testing"
3935 },
3936 "aa": "// Testing",
3937 ".gitignore": "file1\nfile3\n",
3938 }),
3939 )
3940 .await;
3941
3942 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3943 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3944 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3945
3946 cx.update(|_, cx| {
3947 let settings = *ProjectPanelSettings::get_global(cx);
3948 ProjectPanelSettings::override_global(
3949 ProjectPanelSettings {
3950 hide_gitignore: true,
3951 ..settings
3952 },
3953 cx,
3954 );
3955 });
3956
3957 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3958
3959 // Test 1: Visible items should exclude files on gitignore
3960 toggle_expand_dir(&panel, "root/dir1", cx);
3961 select_path(&panel, "root/dir1/file2", cx);
3962 assert_eq!(
3963 visible_entries_as_strings(&panel, 0..10, cx),
3964 &[
3965 "v root",
3966 " v dir1",
3967 " file2 <== selected",
3968 " .gitignore",
3969 " aa"
3970 ],
3971 "Initial state should hide files on .gitignore"
3972 );
3973 submit_deletion(&panel, cx);
3974
3975 // Test 2: Auto selection should go to the parent
3976 assert_eq!(
3977 visible_entries_as_strings(&panel, 0..10, cx),
3978 &[
3979 "v root",
3980 " v dir1 <== selected",
3981 " .gitignore",
3982 " aa"
3983 ],
3984 "Initial state should hide files on .gitignore"
3985 );
3986}
3987
3988#[gpui::test]
3989async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
3990 init_test_with_editor(cx);
3991
3992 let fs = FakeFs::new(cx.executor().clone());
3993 fs.insert_tree(
3994 "/root",
3995 json!({
3996 "dir1": {
3997 "subdir1": {
3998 "a.txt": "",
3999 "b.txt": ""
4000 },
4001 "file1.txt": "",
4002 },
4003 "dir2": {
4004 "subdir2": {
4005 "c.txt": "",
4006 "d.txt": ""
4007 },
4008 "file2.txt": "",
4009 },
4010 "file3.txt": "",
4011 }),
4012 )
4013 .await;
4014
4015 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4016 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4017 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4018 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4019
4020 toggle_expand_dir(&panel, "root/dir1", cx);
4021 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4022 toggle_expand_dir(&panel, "root/dir2", cx);
4023 toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
4024
4025 // Test Case 1: Select and delete nested directory with parent
4026 cx.simulate_modifiers_change(gpui::Modifiers {
4027 control: true,
4028 ..Default::default()
4029 });
4030 select_path_with_mark(&panel, "root/dir1/subdir1", cx);
4031 select_path_with_mark(&panel, "root/dir1", cx);
4032
4033 assert_eq!(
4034 visible_entries_as_strings(&panel, 0..15, cx),
4035 &[
4036 "v root",
4037 " v dir1 <== selected <== marked",
4038 " v subdir1 <== marked",
4039 " a.txt",
4040 " b.txt",
4041 " file1.txt",
4042 " v dir2",
4043 " v subdir2",
4044 " c.txt",
4045 " d.txt",
4046 " file2.txt",
4047 " file3.txt",
4048 ],
4049 "Initial state before deleting nested directory with parent"
4050 );
4051
4052 submit_deletion(&panel, cx);
4053 assert_eq!(
4054 visible_entries_as_strings(&panel, 0..15, cx),
4055 &[
4056 "v root",
4057 " v dir2 <== selected",
4058 " v subdir2",
4059 " c.txt",
4060 " d.txt",
4061 " file2.txt",
4062 " file3.txt",
4063 ],
4064 "Should select next directory after deleting directory with parent"
4065 );
4066
4067 // Test Case 2: Select mixed files and directories across levels
4068 select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx);
4069 select_path_with_mark(&panel, "root/dir2/file2.txt", cx);
4070 select_path_with_mark(&panel, "root/file3.txt", cx);
4071
4072 assert_eq!(
4073 visible_entries_as_strings(&panel, 0..15, cx),
4074 &[
4075 "v root",
4076 " v dir2",
4077 " v subdir2",
4078 " c.txt <== marked",
4079 " d.txt",
4080 " file2.txt <== marked",
4081 " file3.txt <== selected <== marked",
4082 ],
4083 "Initial state before deleting"
4084 );
4085
4086 submit_deletion(&panel, cx);
4087 assert_eq!(
4088 visible_entries_as_strings(&panel, 0..15, cx),
4089 &[
4090 "v root",
4091 " v dir2 <== selected",
4092 " v subdir2",
4093 " d.txt",
4094 ],
4095 "Should select sibling directory"
4096 );
4097}
4098
4099#[gpui::test]
4100async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) {
4101 init_test_with_editor(cx);
4102
4103 let fs = FakeFs::new(cx.executor().clone());
4104 fs.insert_tree(
4105 "/root",
4106 json!({
4107 "dir1": {
4108 "subdir1": {
4109 "a.txt": "",
4110 "b.txt": ""
4111 },
4112 "file1.txt": "",
4113 },
4114 "dir2": {
4115 "subdir2": {
4116 "c.txt": "",
4117 "d.txt": ""
4118 },
4119 "file2.txt": "",
4120 },
4121 "file3.txt": "",
4122 "file4.txt": "",
4123 }),
4124 )
4125 .await;
4126
4127 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4128 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4129 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4130 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4131
4132 toggle_expand_dir(&panel, "root/dir1", cx);
4133 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4134 toggle_expand_dir(&panel, "root/dir2", cx);
4135 toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
4136
4137 // Test Case 1: Select all root files and directories
4138 cx.simulate_modifiers_change(gpui::Modifiers {
4139 control: true,
4140 ..Default::default()
4141 });
4142 select_path_with_mark(&panel, "root/dir1", cx);
4143 select_path_with_mark(&panel, "root/dir2", cx);
4144 select_path_with_mark(&panel, "root/file3.txt", cx);
4145 select_path_with_mark(&panel, "root/file4.txt", cx);
4146 assert_eq!(
4147 visible_entries_as_strings(&panel, 0..20, cx),
4148 &[
4149 "v root",
4150 " v dir1 <== marked",
4151 " v subdir1",
4152 " a.txt",
4153 " b.txt",
4154 " file1.txt",
4155 " v dir2 <== marked",
4156 " v subdir2",
4157 " c.txt",
4158 " d.txt",
4159 " file2.txt",
4160 " file3.txt <== marked",
4161 " file4.txt <== selected <== marked",
4162 ],
4163 "State before deleting all contents"
4164 );
4165
4166 submit_deletion(&panel, cx);
4167 assert_eq!(
4168 visible_entries_as_strings(&panel, 0..20, cx),
4169 &["v root <== selected"],
4170 "Only empty root directory should remain after deleting all contents"
4171 );
4172}
4173
4174#[gpui::test]
4175async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) {
4176 init_test_with_editor(cx);
4177
4178 let fs = FakeFs::new(cx.executor().clone());
4179 fs.insert_tree(
4180 "/root",
4181 json!({
4182 "dir1": {
4183 "subdir1": {
4184 "file_a.txt": "content a",
4185 "file_b.txt": "content b",
4186 },
4187 "subdir2": {
4188 "file_c.txt": "content c",
4189 },
4190 "file1.txt": "content 1",
4191 },
4192 "dir2": {
4193 "file2.txt": "content 2",
4194 },
4195 }),
4196 )
4197 .await;
4198
4199 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4200 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4201 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4202 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4203
4204 toggle_expand_dir(&panel, "root/dir1", cx);
4205 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4206 toggle_expand_dir(&panel, "root/dir2", cx);
4207 cx.simulate_modifiers_change(gpui::Modifiers {
4208 control: true,
4209 ..Default::default()
4210 });
4211
4212 // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory
4213 select_path_with_mark(&panel, "root/dir1", cx);
4214 select_path_with_mark(&panel, "root/dir1/subdir1", cx);
4215 select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx);
4216
4217 assert_eq!(
4218 visible_entries_as_strings(&panel, 0..20, cx),
4219 &[
4220 "v root",
4221 " v dir1 <== marked",
4222 " v subdir1 <== marked",
4223 " file_a.txt <== selected <== marked",
4224 " file_b.txt",
4225 " > subdir2",
4226 " file1.txt",
4227 " v dir2",
4228 " file2.txt",
4229 ],
4230 "State with parent dir, subdir, and file selected"
4231 );
4232 submit_deletion(&panel, cx);
4233 assert_eq!(
4234 visible_entries_as_strings(&panel, 0..20, cx),
4235 &["v root", " v dir2 <== selected", " file2.txt",],
4236 "Only dir2 should remain after deletion"
4237 );
4238}
4239
4240#[gpui::test]
4241async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) {
4242 init_test_with_editor(cx);
4243
4244 let fs = FakeFs::new(cx.executor().clone());
4245 // First worktree
4246 fs.insert_tree(
4247 "/root1",
4248 json!({
4249 "dir1": {
4250 "file1.txt": "content 1",
4251 "file2.txt": "content 2",
4252 },
4253 "dir2": {
4254 "file3.txt": "content 3",
4255 },
4256 }),
4257 )
4258 .await;
4259
4260 // Second worktree
4261 fs.insert_tree(
4262 "/root2",
4263 json!({
4264 "dir3": {
4265 "file4.txt": "content 4",
4266 "file5.txt": "content 5",
4267 },
4268 "file6.txt": "content 6",
4269 }),
4270 )
4271 .await;
4272
4273 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
4274 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4275 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4276 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4277
4278 // Expand all directories for testing
4279 toggle_expand_dir(&panel, "root1/dir1", cx);
4280 toggle_expand_dir(&panel, "root1/dir2", cx);
4281 toggle_expand_dir(&panel, "root2/dir3", cx);
4282
4283 // Test Case 1: Delete files across different worktrees
4284 cx.simulate_modifiers_change(gpui::Modifiers {
4285 control: true,
4286 ..Default::default()
4287 });
4288 select_path_with_mark(&panel, "root1/dir1/file1.txt", cx);
4289 select_path_with_mark(&panel, "root2/dir3/file4.txt", cx);
4290
4291 assert_eq!(
4292 visible_entries_as_strings(&panel, 0..20, cx),
4293 &[
4294 "v root1",
4295 " v dir1",
4296 " file1.txt <== marked",
4297 " file2.txt",
4298 " v dir2",
4299 " file3.txt",
4300 "v root2",
4301 " v dir3",
4302 " file4.txt <== selected <== marked",
4303 " file5.txt",
4304 " file6.txt",
4305 ],
4306 "Initial state with files selected from different worktrees"
4307 );
4308
4309 submit_deletion(&panel, cx);
4310 assert_eq!(
4311 visible_entries_as_strings(&panel, 0..20, cx),
4312 &[
4313 "v root1",
4314 " v dir1",
4315 " file2.txt",
4316 " v dir2",
4317 " file3.txt",
4318 "v root2",
4319 " v dir3",
4320 " file5.txt <== selected",
4321 " file6.txt",
4322 ],
4323 "Should select next file in the last worktree after deletion"
4324 );
4325
4326 // Test Case 2: Delete directories from different worktrees
4327 select_path_with_mark(&panel, "root1/dir1", cx);
4328 select_path_with_mark(&panel, "root2/dir3", cx);
4329
4330 assert_eq!(
4331 visible_entries_as_strings(&panel, 0..20, cx),
4332 &[
4333 "v root1",
4334 " v dir1 <== marked",
4335 " file2.txt",
4336 " v dir2",
4337 " file3.txt",
4338 "v root2",
4339 " v dir3 <== selected <== marked",
4340 " file5.txt",
4341 " file6.txt",
4342 ],
4343 "State with directories marked from different worktrees"
4344 );
4345
4346 submit_deletion(&panel, cx);
4347 assert_eq!(
4348 visible_entries_as_strings(&panel, 0..20, cx),
4349 &[
4350 "v root1",
4351 " v dir2",
4352 " file3.txt",
4353 "v root2",
4354 " file6.txt <== selected",
4355 ],
4356 "Should select remaining file in last worktree after directory deletion"
4357 );
4358
4359 // Test Case 4: Delete all remaining files except roots
4360 select_path_with_mark(&panel, "root1/dir2/file3.txt", cx);
4361 select_path_with_mark(&panel, "root2/file6.txt", cx);
4362
4363 assert_eq!(
4364 visible_entries_as_strings(&panel, 0..20, cx),
4365 &[
4366 "v root1",
4367 " v dir2",
4368 " file3.txt <== marked",
4369 "v root2",
4370 " file6.txt <== selected <== marked",
4371 ],
4372 "State with all remaining files marked"
4373 );
4374
4375 submit_deletion(&panel, cx);
4376 assert_eq!(
4377 visible_entries_as_strings(&panel, 0..20, cx),
4378 &["v root1", " v dir2", "v root2 <== selected"],
4379 "Second parent root should be selected after deleting"
4380 );
4381}
4382
4383#[gpui::test]
4384async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) {
4385 init_test_with_editor(cx);
4386
4387 let fs = FakeFs::new(cx.executor().clone());
4388 fs.insert_tree(
4389 "/root",
4390 json!({
4391 "dir1": {
4392 "file1.txt": "",
4393 "file2.txt": "",
4394 "file3.txt": "",
4395 },
4396 "dir2": {
4397 "file4.txt": "",
4398 "file5.txt": "",
4399 },
4400 }),
4401 )
4402 .await;
4403
4404 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4405 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4406 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4407 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4408
4409 toggle_expand_dir(&panel, "root/dir1", cx);
4410 toggle_expand_dir(&panel, "root/dir2", cx);
4411
4412 cx.simulate_modifiers_change(gpui::Modifiers {
4413 control: true,
4414 ..Default::default()
4415 });
4416
4417 select_path_with_mark(&panel, "root/dir1/file2.txt", cx);
4418 select_path(&panel, "root/dir1/file1.txt", cx);
4419
4420 assert_eq!(
4421 visible_entries_as_strings(&panel, 0..15, cx),
4422 &[
4423 "v root",
4424 " v dir1",
4425 " file1.txt <== selected",
4426 " file2.txt <== marked",
4427 " file3.txt",
4428 " v dir2",
4429 " file4.txt",
4430 " file5.txt",
4431 ],
4432 "Initial state with one marked entry and different selection"
4433 );
4434
4435 // Delete should operate on the selected entry (file1.txt)
4436 submit_deletion(&panel, cx);
4437 assert_eq!(
4438 visible_entries_as_strings(&panel, 0..15, cx),
4439 &[
4440 "v root",
4441 " v dir1",
4442 " file2.txt <== selected <== marked",
4443 " file3.txt",
4444 " v dir2",
4445 " file4.txt",
4446 " file5.txt",
4447 ],
4448 "Should delete selected file, not marked file"
4449 );
4450
4451 select_path_with_mark(&panel, "root/dir1/file3.txt", cx);
4452 select_path_with_mark(&panel, "root/dir2/file4.txt", cx);
4453 select_path(&panel, "root/dir2/file5.txt", cx);
4454
4455 assert_eq!(
4456 visible_entries_as_strings(&panel, 0..15, cx),
4457 &[
4458 "v root",
4459 " v dir1",
4460 " file2.txt <== marked",
4461 " file3.txt <== marked",
4462 " v dir2",
4463 " file4.txt <== marked",
4464 " file5.txt <== selected",
4465 ],
4466 "Initial state with multiple marked entries and different selection"
4467 );
4468
4469 // Delete should operate on all marked entries, ignoring the selection
4470 submit_deletion(&panel, cx);
4471 assert_eq!(
4472 visible_entries_as_strings(&panel, 0..15, cx),
4473 &[
4474 "v root",
4475 " v dir1",
4476 " v dir2",
4477 " file5.txt <== selected",
4478 ],
4479 "Should delete all marked files, leaving only the selected file"
4480 );
4481}
4482
4483#[gpui::test]
4484async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) {
4485 init_test_with_editor(cx);
4486
4487 let fs = FakeFs::new(cx.executor().clone());
4488 fs.insert_tree(
4489 "/root_b",
4490 json!({
4491 "dir1": {
4492 "file1.txt": "content 1",
4493 "file2.txt": "content 2",
4494 },
4495 }),
4496 )
4497 .await;
4498
4499 fs.insert_tree(
4500 "/root_c",
4501 json!({
4502 "dir2": {},
4503 }),
4504 )
4505 .await;
4506
4507 let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await;
4508 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4509 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4510 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4511
4512 toggle_expand_dir(&panel, "root_b/dir1", cx);
4513 toggle_expand_dir(&panel, "root_c/dir2", cx);
4514
4515 cx.simulate_modifiers_change(gpui::Modifiers {
4516 control: true,
4517 ..Default::default()
4518 });
4519 select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx);
4520 select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx);
4521
4522 assert_eq!(
4523 visible_entries_as_strings(&panel, 0..20, cx),
4524 &[
4525 "v root_b",
4526 " v dir1",
4527 " file1.txt <== marked",
4528 " file2.txt <== selected <== marked",
4529 "v root_c",
4530 " v dir2",
4531 ],
4532 "Initial state with files marked in root_b"
4533 );
4534
4535 submit_deletion(&panel, cx);
4536 assert_eq!(
4537 visible_entries_as_strings(&panel, 0..20, cx),
4538 &[
4539 "v root_b",
4540 " v dir1 <== selected",
4541 "v root_c",
4542 " v dir2",
4543 ],
4544 "After deletion in root_b as it's last deletion, selection should be in root_b"
4545 );
4546
4547 select_path_with_mark(&panel, "root_c/dir2", cx);
4548
4549 submit_deletion(&panel, cx);
4550 assert_eq!(
4551 visible_entries_as_strings(&panel, 0..20, cx),
4552 &["v root_b", " v dir1", "v root_c <== selected",],
4553 "After deleting from root_c, it should remain in root_c"
4554 );
4555}
4556
4557fn toggle_expand_dir(
4558 panel: &Entity<ProjectPanel>,
4559 path: impl AsRef<Path>,
4560 cx: &mut VisualTestContext,
4561) {
4562 let path = path.as_ref();
4563 panel.update_in(cx, |panel, window, cx| {
4564 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4565 let worktree = worktree.read(cx);
4566 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4567 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
4568 panel.toggle_expanded(entry_id, window, cx);
4569 return;
4570 }
4571 }
4572 panic!("no worktree for path {:?}", path);
4573 });
4574}
4575
4576#[gpui::test]
4577async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) {
4578 init_test_with_editor(cx);
4579
4580 let fs = FakeFs::new(cx.executor().clone());
4581 fs.insert_tree(
4582 path!("/root"),
4583 json!({
4584 ".gitignore": "**/ignored_dir\n**/ignored_nested",
4585 "dir1": {
4586 "empty1": {
4587 "empty2": {
4588 "empty3": {
4589 "file.txt": ""
4590 }
4591 }
4592 },
4593 "subdir1": {
4594 "file1.txt": "",
4595 "file2.txt": "",
4596 "ignored_nested": {
4597 "ignored_file.txt": ""
4598 }
4599 },
4600 "ignored_dir": {
4601 "subdir": {
4602 "deep_file.txt": ""
4603 }
4604 }
4605 }
4606 }),
4607 )
4608 .await;
4609
4610 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4611 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4612 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4613
4614 // Test 1: When auto-fold is enabled
4615 cx.update(|_, cx| {
4616 let settings = *ProjectPanelSettings::get_global(cx);
4617 ProjectPanelSettings::override_global(
4618 ProjectPanelSettings {
4619 auto_fold_dirs: true,
4620 ..settings
4621 },
4622 cx,
4623 );
4624 });
4625
4626 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4627
4628 assert_eq!(
4629 visible_entries_as_strings(&panel, 0..20, cx),
4630 &["v root", " > dir1", " .gitignore",],
4631 "Initial state should show collapsed root structure"
4632 );
4633
4634 toggle_expand_dir(&panel, "root/dir1", cx);
4635 assert_eq!(
4636 visible_entries_as_strings(&panel, 0..20, cx),
4637 &[
4638 separator!("v root"),
4639 separator!(" v dir1 <== selected"),
4640 separator!(" > empty1/empty2/empty3"),
4641 separator!(" > ignored_dir"),
4642 separator!(" > subdir1"),
4643 separator!(" .gitignore"),
4644 ],
4645 "Should show first level with auto-folded dirs and ignored dir visible"
4646 );
4647
4648 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4649 panel.update(cx, |panel, cx| {
4650 let project = panel.project.read(cx);
4651 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4652 panel.expand_all_for_entry(worktree.id(), entry_id, cx);
4653 panel.update_visible_entries(None, cx);
4654 });
4655 cx.run_until_parked();
4656
4657 assert_eq!(
4658 visible_entries_as_strings(&panel, 0..20, cx),
4659 &[
4660 separator!("v root"),
4661 separator!(" v dir1 <== selected"),
4662 separator!(" v empty1"),
4663 separator!(" v empty2"),
4664 separator!(" v empty3"),
4665 separator!(" file.txt"),
4666 separator!(" > ignored_dir"),
4667 separator!(" v subdir1"),
4668 separator!(" > ignored_nested"),
4669 separator!(" file1.txt"),
4670 separator!(" file2.txt"),
4671 separator!(" .gitignore"),
4672 ],
4673 "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested"
4674 );
4675
4676 // Test 2: When auto-fold is disabled
4677 cx.update(|_, cx| {
4678 let settings = *ProjectPanelSettings::get_global(cx);
4679 ProjectPanelSettings::override_global(
4680 ProjectPanelSettings {
4681 auto_fold_dirs: false,
4682 ..settings
4683 },
4684 cx,
4685 );
4686 });
4687
4688 panel.update_in(cx, |panel, window, cx| {
4689 panel.collapse_all_entries(&CollapseAllEntries, window, cx);
4690 });
4691
4692 toggle_expand_dir(&panel, "root/dir1", cx);
4693 assert_eq!(
4694 visible_entries_as_strings(&panel, 0..20, cx),
4695 &[
4696 separator!("v root"),
4697 separator!(" v dir1 <== selected"),
4698 separator!(" > empty1"),
4699 separator!(" > ignored_dir"),
4700 separator!(" > subdir1"),
4701 separator!(" .gitignore"),
4702 ],
4703 "With auto-fold disabled: should show all directories separately"
4704 );
4705
4706 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4707 panel.update(cx, |panel, cx| {
4708 let project = panel.project.read(cx);
4709 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4710 panel.expand_all_for_entry(worktree.id(), entry_id, cx);
4711 panel.update_visible_entries(None, cx);
4712 });
4713 cx.run_until_parked();
4714
4715 assert_eq!(
4716 visible_entries_as_strings(&panel, 0..20, cx),
4717 &[
4718 separator!("v root"),
4719 separator!(" v dir1 <== selected"),
4720 separator!(" v empty1"),
4721 separator!(" v empty2"),
4722 separator!(" v empty3"),
4723 separator!(" file.txt"),
4724 separator!(" > ignored_dir"),
4725 separator!(" v subdir1"),
4726 separator!(" > ignored_nested"),
4727 separator!(" file1.txt"),
4728 separator!(" file2.txt"),
4729 separator!(" .gitignore"),
4730 ],
4731 "After expand_all without auto-fold: should expand all dirs normally, \
4732 expand ignored_dir itself but not its subdirs, and not expand ignored_nested"
4733 );
4734
4735 // Test 3: When explicitly called on ignored directory
4736 let ignored_dir_entry = find_project_entry(&panel, "root/dir1/ignored_dir", cx).unwrap();
4737 panel.update(cx, |panel, cx| {
4738 let project = panel.project.read(cx);
4739 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4740 panel.expand_all_for_entry(worktree.id(), ignored_dir_entry, cx);
4741 panel.update_visible_entries(None, cx);
4742 });
4743 cx.run_until_parked();
4744
4745 assert_eq!(
4746 visible_entries_as_strings(&panel, 0..20, cx),
4747 &[
4748 separator!("v root"),
4749 separator!(" v dir1 <== selected"),
4750 separator!(" v empty1"),
4751 separator!(" v empty2"),
4752 separator!(" v empty3"),
4753 separator!(" file.txt"),
4754 separator!(" v ignored_dir"),
4755 separator!(" v subdir"),
4756 separator!(" deep_file.txt"),
4757 separator!(" v subdir1"),
4758 separator!(" > ignored_nested"),
4759 separator!(" file1.txt"),
4760 separator!(" file2.txt"),
4761 separator!(" .gitignore"),
4762 ],
4763 "After expand_all on ignored_dir: should expand all contents of the ignored directory"
4764 );
4765}
4766
4767#[gpui::test]
4768async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) {
4769 init_test(cx);
4770
4771 let fs = FakeFs::new(cx.executor().clone());
4772 fs.insert_tree(
4773 path!("/root"),
4774 json!({
4775 "dir1": {
4776 "subdir1": {
4777 "nested1": {
4778 "file1.txt": "",
4779 "file2.txt": ""
4780 },
4781 },
4782 "subdir2": {
4783 "file4.txt": ""
4784 }
4785 },
4786 "dir2": {
4787 "single_file": {
4788 "file5.txt": ""
4789 }
4790 }
4791 }),
4792 )
4793 .await;
4794
4795 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4796 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4797 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4798
4799 // Test 1: Basic collapsing
4800 {
4801 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4802
4803 toggle_expand_dir(&panel, "root/dir1", cx);
4804 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4805 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4806 toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
4807
4808 assert_eq!(
4809 visible_entries_as_strings(&panel, 0..20, cx),
4810 &[
4811 separator!("v root"),
4812 separator!(" v dir1"),
4813 separator!(" v subdir1"),
4814 separator!(" v nested1"),
4815 separator!(" file1.txt"),
4816 separator!(" file2.txt"),
4817 separator!(" v subdir2 <== selected"),
4818 separator!(" file4.txt"),
4819 separator!(" > dir2"),
4820 ],
4821 "Initial state with everything expanded"
4822 );
4823
4824 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4825 panel.update(cx, |panel, cx| {
4826 let project = panel.project.read(cx);
4827 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4828 panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4829 panel.update_visible_entries(None, cx);
4830 });
4831
4832 assert_eq!(
4833 visible_entries_as_strings(&panel, 0..20, cx),
4834 &["v root", " > dir1", " > dir2",],
4835 "All subdirs under dir1 should be collapsed"
4836 );
4837 }
4838
4839 // Test 2: With auto-fold enabled
4840 {
4841 cx.update(|_, cx| {
4842 let settings = *ProjectPanelSettings::get_global(cx);
4843 ProjectPanelSettings::override_global(
4844 ProjectPanelSettings {
4845 auto_fold_dirs: true,
4846 ..settings
4847 },
4848 cx,
4849 );
4850 });
4851
4852 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4853
4854 toggle_expand_dir(&panel, "root/dir1", cx);
4855 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4856 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4857
4858 assert_eq!(
4859 visible_entries_as_strings(&panel, 0..20, cx),
4860 &[
4861 separator!("v root"),
4862 separator!(" v dir1"),
4863 separator!(" v subdir1/nested1 <== selected"),
4864 separator!(" file1.txt"),
4865 separator!(" file2.txt"),
4866 separator!(" > subdir2"),
4867 separator!(" > dir2/single_file"),
4868 ],
4869 "Initial state with some dirs expanded"
4870 );
4871
4872 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4873 panel.update(cx, |panel, cx| {
4874 let project = panel.project.read(cx);
4875 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4876 panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4877 });
4878
4879 toggle_expand_dir(&panel, "root/dir1", cx);
4880
4881 assert_eq!(
4882 visible_entries_as_strings(&panel, 0..20, cx),
4883 &[
4884 separator!("v root"),
4885 separator!(" v dir1 <== selected"),
4886 separator!(" > subdir1/nested1"),
4887 separator!(" > subdir2"),
4888 separator!(" > dir2/single_file"),
4889 ],
4890 "Subdirs should be collapsed and folded with auto-fold enabled"
4891 );
4892 }
4893
4894 // Test 3: With auto-fold disabled
4895 {
4896 cx.update(|_, cx| {
4897 let settings = *ProjectPanelSettings::get_global(cx);
4898 ProjectPanelSettings::override_global(
4899 ProjectPanelSettings {
4900 auto_fold_dirs: false,
4901 ..settings
4902 },
4903 cx,
4904 );
4905 });
4906
4907 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4908
4909 toggle_expand_dir(&panel, "root/dir1", cx);
4910 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4911 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4912
4913 assert_eq!(
4914 visible_entries_as_strings(&panel, 0..20, cx),
4915 &[
4916 separator!("v root"),
4917 separator!(" v dir1"),
4918 separator!(" v subdir1"),
4919 separator!(" v nested1 <== selected"),
4920 separator!(" file1.txt"),
4921 separator!(" file2.txt"),
4922 separator!(" > subdir2"),
4923 separator!(" > dir2"),
4924 ],
4925 "Initial state with some dirs expanded and auto-fold disabled"
4926 );
4927
4928 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4929 panel.update(cx, |panel, cx| {
4930 let project = panel.project.read(cx);
4931 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4932 panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4933 });
4934
4935 toggle_expand_dir(&panel, "root/dir1", cx);
4936
4937 assert_eq!(
4938 visible_entries_as_strings(&panel, 0..20, cx),
4939 &[
4940 separator!("v root"),
4941 separator!(" v dir1 <== selected"),
4942 separator!(" > subdir1"),
4943 separator!(" > subdir2"),
4944 separator!(" > dir2"),
4945 ],
4946 "Subdirs should be collapsed but not folded with auto-fold disabled"
4947 );
4948 }
4949}
4950
4951#[gpui::test]
4952async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) {
4953 init_test(cx);
4954
4955 let fs = FakeFs::new(cx.executor().clone());
4956 fs.insert_tree(
4957 path!("/root"),
4958 json!({
4959 "dir1": {
4960 "file1.txt": "",
4961 },
4962 }),
4963 )
4964 .await;
4965
4966 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4967 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4968 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4969
4970 let panel = workspace
4971 .update(cx, |workspace, window, cx| {
4972 let panel = ProjectPanel::new(workspace, window, cx);
4973 workspace.add_panel(panel.clone(), window, cx);
4974 panel
4975 })
4976 .unwrap();
4977
4978 #[rustfmt::skip]
4979 assert_eq!(
4980 visible_entries_as_strings(&panel, 0..20, cx),
4981 &[
4982 separator!("v root"),
4983 separator!(" > dir1"),
4984 ],
4985 "Initial state with nothing selected"
4986 );
4987
4988 panel.update_in(cx, |panel, window, cx| {
4989 panel.new_file(&NewFile, window, cx);
4990 });
4991 panel.update_in(cx, |panel, window, cx| {
4992 assert!(panel.filename_editor.read(cx).is_focused(window));
4993 });
4994 panel
4995 .update_in(cx, |panel, window, cx| {
4996 panel.filename_editor.update(cx, |editor, cx| {
4997 editor.set_text("hello_from_no_selections", window, cx)
4998 });
4999 panel.confirm_edit(window, cx).unwrap()
5000 })
5001 .await
5002 .unwrap();
5003
5004 #[rustfmt::skip]
5005 assert_eq!(
5006 visible_entries_as_strings(&panel, 0..20, cx),
5007 &[
5008 separator!("v root"),
5009 separator!(" > dir1"),
5010 separator!(" hello_from_no_selections <== selected <== marked"),
5011 ],
5012 "A new file is created under the root directory"
5013 );
5014}
5015
5016fn select_path(panel: &Entity<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
5017 let path = path.as_ref();
5018 panel.update(cx, |panel, cx| {
5019 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
5020 let worktree = worktree.read(cx);
5021 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
5022 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
5023 panel.selection = Some(crate::SelectedEntry {
5024 worktree_id: worktree.id(),
5025 entry_id,
5026 });
5027 return;
5028 }
5029 }
5030 panic!("no worktree for path {:?}", path);
5031 });
5032}
5033
5034fn select_path_with_mark(
5035 panel: &Entity<ProjectPanel>,
5036 path: impl AsRef<Path>,
5037 cx: &mut VisualTestContext,
5038) {
5039 let path = path.as_ref();
5040 panel.update(cx, |panel, cx| {
5041 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
5042 let worktree = worktree.read(cx);
5043 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
5044 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
5045 let entry = crate::SelectedEntry {
5046 worktree_id: worktree.id(),
5047 entry_id,
5048 };
5049 if !panel.marked_entries.contains(&entry) {
5050 panel.marked_entries.insert(entry);
5051 }
5052 panel.selection = Some(entry);
5053 return;
5054 }
5055 }
5056 panic!("no worktree for path {:?}", path);
5057 });
5058}
5059
5060fn find_project_entry(
5061 panel: &Entity<ProjectPanel>,
5062 path: impl AsRef<Path>,
5063 cx: &mut VisualTestContext,
5064) -> Option<ProjectEntryId> {
5065 let path = path.as_ref();
5066 panel.update(cx, |panel, cx| {
5067 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
5068 let worktree = worktree.read(cx);
5069 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
5070 return worktree.entry_for_path(relative_path).map(|entry| entry.id);
5071 }
5072 }
5073 panic!("no worktree for path {path:?}");
5074 })
5075}
5076
5077fn visible_entries_as_strings(
5078 panel: &Entity<ProjectPanel>,
5079 range: Range<usize>,
5080 cx: &mut VisualTestContext,
5081) -> Vec<String> {
5082 let mut result = Vec::new();
5083 let mut project_entries = HashSet::default();
5084 let mut has_editor = false;
5085
5086 panel.update_in(cx, |panel, window, cx| {
5087 panel.for_each_visible_entry(range, window, cx, |project_entry, details, _, _| {
5088 if details.is_editing {
5089 assert!(!has_editor, "duplicate editor entry");
5090 has_editor = true;
5091 } else {
5092 assert!(
5093 project_entries.insert(project_entry),
5094 "duplicate project entry {:?} {:?}",
5095 project_entry,
5096 details
5097 );
5098 }
5099
5100 let indent = " ".repeat(details.depth);
5101 let icon = if details.kind.is_dir() {
5102 if details.is_expanded { "v " } else { "> " }
5103 } else {
5104 " "
5105 };
5106 let name = if details.is_editing {
5107 format!("[EDITOR: '{}']", details.filename)
5108 } else if details.is_processing {
5109 format!("[PROCESSING: '{}']", details.filename)
5110 } else {
5111 details.filename.clone()
5112 };
5113 let selected = if details.is_selected {
5114 " <== selected"
5115 } else {
5116 ""
5117 };
5118 let marked = if details.is_marked {
5119 " <== marked"
5120 } else {
5121 ""
5122 };
5123
5124 result.push(format!("{indent}{icon}{name}{selected}{marked}"));
5125 });
5126 });
5127
5128 result
5129}
5130
5131fn init_test(cx: &mut TestAppContext) {
5132 cx.update(|cx| {
5133 let settings_store = SettingsStore::test(cx);
5134 cx.set_global(settings_store);
5135 init_settings(cx);
5136 theme::init(theme::LoadThemes::JustBase, cx);
5137 language::init(cx);
5138 editor::init_settings(cx);
5139 crate::init(cx);
5140 workspace::init_settings(cx);
5141 client::init_settings(cx);
5142 Project::init_settings(cx);
5143
5144 cx.update_global::<SettingsStore, _>(|store, cx| {
5145 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5146 project_panel_settings.auto_fold_dirs = Some(false);
5147 });
5148 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5149 worktree_settings.file_scan_exclusions = Some(Vec::new());
5150 });
5151 });
5152 });
5153}
5154
5155fn init_test_with_editor(cx: &mut TestAppContext) {
5156 cx.update(|cx| {
5157 let app_state = AppState::test(cx);
5158 theme::init(theme::LoadThemes::JustBase, cx);
5159 init_settings(cx);
5160 language::init(cx);
5161 editor::init(cx);
5162 crate::init(cx);
5163 workspace::init(app_state.clone(), cx);
5164 Project::init_settings(cx);
5165
5166 cx.update_global::<SettingsStore, _>(|store, cx| {
5167 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5168 project_panel_settings.auto_fold_dirs = Some(false);
5169 });
5170 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5171 worktree_settings.file_scan_exclusions = Some(Vec::new());
5172 });
5173 });
5174 });
5175}
5176
5177fn ensure_single_file_is_opened(
5178 window: &WindowHandle<Workspace>,
5179 expected_path: &str,
5180 cx: &mut TestAppContext,
5181) {
5182 window
5183 .update(cx, |workspace, _, cx| {
5184 let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
5185 assert_eq!(worktrees.len(), 1);
5186 let worktree_id = worktrees[0].read(cx).id();
5187
5188 let open_project_paths = workspace
5189 .panes()
5190 .iter()
5191 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
5192 .collect::<Vec<_>>();
5193 assert_eq!(
5194 open_project_paths,
5195 vec![ProjectPath {
5196 worktree_id,
5197 path: Arc::from(Path::new(expected_path))
5198 }],
5199 "Should have opened file, selected in project panel"
5200 );
5201 })
5202 .unwrap();
5203}
5204
5205fn submit_deletion(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
5206 assert!(
5207 !cx.has_pending_prompt(),
5208 "Should have no prompts before the deletion"
5209 );
5210 panel.update_in(cx, |panel, window, cx| {
5211 panel.delete(&Delete { skip_prompt: false }, window, cx)
5212 });
5213 assert!(
5214 cx.has_pending_prompt(),
5215 "Should have a prompt after the deletion"
5216 );
5217 cx.simulate_prompt_answer("Delete");
5218 assert!(
5219 !cx.has_pending_prompt(),
5220 "Should have no prompts after prompt was replied to"
5221 );
5222 cx.executor().run_until_parked();
5223}
5224
5225fn submit_deletion_skipping_prompt(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
5226 assert!(
5227 !cx.has_pending_prompt(),
5228 "Should have no prompts before the deletion"
5229 );
5230 panel.update_in(cx, |panel, window, cx| {
5231 panel.delete(&Delete { skip_prompt: true }, window, cx)
5232 });
5233 assert!(!cx.has_pending_prompt(), "Should have received no prompts");
5234 cx.executor().run_until_parked();
5235}
5236
5237fn ensure_no_open_items_and_panes(workspace: &WindowHandle<Workspace>, cx: &mut VisualTestContext) {
5238 assert!(
5239 !cx.has_pending_prompt(),
5240 "Should have no prompts after deletion operation closes the file"
5241 );
5242 workspace
5243 .read_with(cx, |workspace, cx| {
5244 let open_project_paths = workspace
5245 .panes()
5246 .iter()
5247 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
5248 .collect::<Vec<_>>();
5249 assert!(
5250 open_project_paths.is_empty(),
5251 "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
5252 );
5253 })
5254 .unwrap();
5255}
5256
5257struct TestProjectItemView {
5258 focus_handle: FocusHandle,
5259 path: ProjectPath,
5260}
5261
5262struct TestProjectItem {
5263 path: ProjectPath,
5264}
5265
5266impl project::ProjectItem for TestProjectItem {
5267 fn try_open(
5268 _project: &Entity<Project>,
5269 path: &ProjectPath,
5270 cx: &mut App,
5271 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
5272 let path = path.clone();
5273 Some(cx.spawn(async move |cx| cx.new(|_| Self { path })))
5274 }
5275
5276 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
5277 None
5278 }
5279
5280 fn project_path(&self, _: &App) -> Option<ProjectPath> {
5281 Some(self.path.clone())
5282 }
5283
5284 fn is_dirty(&self) -> bool {
5285 false
5286 }
5287}
5288
5289impl ProjectItem for TestProjectItemView {
5290 type Item = TestProjectItem;
5291
5292 fn for_project_item(
5293 _: Entity<Project>,
5294 _: Option<&Pane>,
5295 project_item: Entity<Self::Item>,
5296 _: &mut Window,
5297 cx: &mut Context<Self>,
5298 ) -> Self
5299 where
5300 Self: Sized,
5301 {
5302 Self {
5303 path: project_item.update(cx, |project_item, _| project_item.path.clone()),
5304 focus_handle: cx.focus_handle(),
5305 }
5306 }
5307}
5308
5309impl Item for TestProjectItemView {
5310 type Event = ();
5311
5312 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
5313 "Test".into()
5314 }
5315}
5316
5317impl EventEmitter<()> for TestProjectItemView {}
5318
5319impl Focusable for TestProjectItemView {
5320 fn focus_handle(&self, _: &App) -> FocusHandle {
5321 self.focus_handle.clone()
5322 }
5323}
5324
5325impl Render for TestProjectItemView {
5326 fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5327 Empty
5328 }
5329}