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.simulate_modifiers_change(modifiers_with_shift);
2754 cx.update(|window, cx| {
2755 panel.update(cx, |this, cx| {
2756 this.select_next(&Default::default(), window, cx);
2757 })
2758 });
2759 assert_eq!(
2760 visible_entries_as_strings(&panel, 0..10, cx),
2761 &[
2762 "v project_root",
2763 " v dir_1",
2764 " v nested_dir",
2765 " file_a.py",
2766 " file_1.py <== selected <== marked",
2767 ]
2768 );
2769 cx.update(|window, cx| {
2770 panel.update(cx, |this, cx| {
2771 this.select_previous(&Default::default(), window, cx);
2772 })
2773 });
2774 assert_eq!(
2775 visible_entries_as_strings(&panel, 0..10, cx),
2776 &[
2777 "v project_root",
2778 " v dir_1",
2779 " v nested_dir",
2780 " file_a.py <== selected <== marked",
2781 " file_1.py <== marked",
2782 ]
2783 );
2784 cx.update(|window, cx| {
2785 panel.update(cx, |this, cx| {
2786 let drag = DraggedSelection {
2787 active_selection: this.selection.unwrap(),
2788 marked_selections: Arc::new(this.marked_entries.clone()),
2789 };
2790 let target_entry = this
2791 .project
2792 .read(cx)
2793 .entry_for_path(&(worktree_id, "").into(), cx)
2794 .unwrap();
2795 this.drag_onto(&drag, target_entry.id, false, window, cx);
2796 });
2797 });
2798 cx.run_until_parked();
2799 assert_eq!(
2800 visible_entries_as_strings(&panel, 0..10, cx),
2801 &[
2802 "v project_root",
2803 " v dir_1",
2804 " v nested_dir",
2805 " file_1.py <== marked",
2806 " file_a.py <== selected <== marked",
2807 ]
2808 );
2809 // ESC clears out all marks
2810 cx.update(|window, cx| {
2811 panel.update(cx, |this, cx| {
2812 this.cancel(&menu::Cancel, window, cx);
2813 })
2814 });
2815 assert_eq!(
2816 visible_entries_as_strings(&panel, 0..10, cx),
2817 &[
2818 "v project_root",
2819 " v dir_1",
2820 " v nested_dir",
2821 " file_1.py",
2822 " file_a.py <== selected",
2823 ]
2824 );
2825 // ESC clears out all marks
2826 cx.update(|window, cx| {
2827 panel.update(cx, |this, cx| {
2828 this.select_previous(&SelectPrevious, window, cx);
2829 this.select_next(&SelectNext, window, cx);
2830 })
2831 });
2832 assert_eq!(
2833 visible_entries_as_strings(&panel, 0..10, cx),
2834 &[
2835 "v project_root",
2836 " v dir_1",
2837 " v nested_dir",
2838 " file_1.py <== marked",
2839 " file_a.py <== selected <== marked",
2840 ]
2841 );
2842 cx.simulate_modifiers_change(Default::default());
2843 cx.update(|window, cx| {
2844 panel.update(cx, |this, cx| {
2845 this.cut(&Cut, window, cx);
2846 this.select_previous(&SelectPrevious, window, cx);
2847 this.select_previous(&SelectPrevious, window, cx);
2848
2849 this.paste(&Paste, window, cx);
2850 // this.expand_selected_entry(&ExpandSelectedEntry, cx);
2851 })
2852 });
2853 cx.run_until_parked();
2854 assert_eq!(
2855 visible_entries_as_strings(&panel, 0..10, cx),
2856 &[
2857 "v project_root",
2858 " v dir_1",
2859 " v nested_dir",
2860 " file_1.py <== marked",
2861 " file_a.py <== selected <== marked",
2862 ]
2863 );
2864 cx.simulate_modifiers_change(modifiers_with_shift);
2865 cx.update(|window, cx| {
2866 panel.update(cx, |this, cx| {
2867 this.expand_selected_entry(&Default::default(), window, cx);
2868 this.select_next(&SelectNext, window, cx);
2869 this.select_next(&SelectNext, window, cx);
2870 })
2871 });
2872 submit_deletion(&panel, cx);
2873 assert_eq!(
2874 visible_entries_as_strings(&panel, 0..10, cx),
2875 &[
2876 "v project_root",
2877 " v dir_1",
2878 " v nested_dir <== selected",
2879 ]
2880 );
2881}
2882#[gpui::test]
2883async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
2884 init_test_with_editor(cx);
2885 cx.update(|cx| {
2886 cx.update_global::<SettingsStore, _>(|store, cx| {
2887 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
2888 worktree_settings.file_scan_exclusions = Some(Vec::new());
2889 });
2890 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
2891 project_panel_settings.auto_reveal_entries = Some(false)
2892 });
2893 })
2894 });
2895
2896 let fs = FakeFs::new(cx.background_executor.clone());
2897 fs.insert_tree(
2898 "/project_root",
2899 json!({
2900 ".git": {},
2901 ".gitignore": "**/gitignored_dir",
2902 "dir_1": {
2903 "file_1.py": "# File 1_1 contents",
2904 "file_2.py": "# File 1_2 contents",
2905 "file_3.py": "# File 1_3 contents",
2906 "gitignored_dir": {
2907 "file_a.py": "# File contents",
2908 "file_b.py": "# File contents",
2909 "file_c.py": "# File contents",
2910 },
2911 },
2912 "dir_2": {
2913 "file_1.py": "# File 2_1 contents",
2914 "file_2.py": "# File 2_2 contents",
2915 "file_3.py": "# File 2_3 contents",
2916 }
2917 }),
2918 )
2919 .await;
2920
2921 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2922 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2923 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2924 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2925
2926 assert_eq!(
2927 visible_entries_as_strings(&panel, 0..20, cx),
2928 &[
2929 "v project_root",
2930 " > .git",
2931 " > dir_1",
2932 " > dir_2",
2933 " .gitignore",
2934 ]
2935 );
2936
2937 let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
2938 .expect("dir 1 file is not ignored and should have an entry");
2939 let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
2940 .expect("dir 2 file is not ignored and should have an entry");
2941 let gitignored_dir_file =
2942 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
2943 assert_eq!(
2944 gitignored_dir_file, None,
2945 "File in the gitignored dir should not have an entry before its dir is toggled"
2946 );
2947
2948 toggle_expand_dir(&panel, "project_root/dir_1", cx);
2949 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
2950 cx.executor().run_until_parked();
2951 assert_eq!(
2952 visible_entries_as_strings(&panel, 0..20, cx),
2953 &[
2954 "v project_root",
2955 " > .git",
2956 " v dir_1",
2957 " v gitignored_dir <== selected",
2958 " file_a.py",
2959 " file_b.py",
2960 " file_c.py",
2961 " file_1.py",
2962 " file_2.py",
2963 " file_3.py",
2964 " > dir_2",
2965 " .gitignore",
2966 ],
2967 "Should show gitignored dir file list in the project panel"
2968 );
2969 let gitignored_dir_file =
2970 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
2971 .expect("after gitignored dir got opened, a file entry should be present");
2972
2973 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
2974 toggle_expand_dir(&panel, "project_root/dir_1", cx);
2975 assert_eq!(
2976 visible_entries_as_strings(&panel, 0..20, cx),
2977 &[
2978 "v project_root",
2979 " > .git",
2980 " > dir_1 <== selected",
2981 " > dir_2",
2982 " .gitignore",
2983 ],
2984 "Should hide all dir contents again and prepare for the auto reveal test"
2985 );
2986
2987 for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
2988 panel.update(cx, |panel, cx| {
2989 panel.project.update(cx, |_, cx| {
2990 cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
2991 })
2992 });
2993 cx.run_until_parked();
2994 assert_eq!(
2995 visible_entries_as_strings(&panel, 0..20, cx),
2996 &[
2997 "v project_root",
2998 " > .git",
2999 " > dir_1 <== selected",
3000 " > dir_2",
3001 " .gitignore",
3002 ],
3003 "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
3004 );
3005 }
3006
3007 cx.update(|_, cx| {
3008 cx.update_global::<SettingsStore, _>(|store, cx| {
3009 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3010 project_panel_settings.auto_reveal_entries = Some(true)
3011 });
3012 })
3013 });
3014
3015 panel.update(cx, |panel, cx| {
3016 panel.project.update(cx, |_, cx| {
3017 cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
3018 })
3019 });
3020 cx.run_until_parked();
3021 assert_eq!(
3022 visible_entries_as_strings(&panel, 0..20, cx),
3023 &[
3024 "v project_root",
3025 " > .git",
3026 " v dir_1",
3027 " > gitignored_dir",
3028 " file_1.py <== selected <== marked",
3029 " file_2.py",
3030 " file_3.py",
3031 " > dir_2",
3032 " .gitignore",
3033 ],
3034 "When auto reveal is enabled, not ignored dir_1 entry should be revealed"
3035 );
3036
3037 panel.update(cx, |panel, cx| {
3038 panel.project.update(cx, |_, cx| {
3039 cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
3040 })
3041 });
3042 cx.run_until_parked();
3043 assert_eq!(
3044 visible_entries_as_strings(&panel, 0..20, cx),
3045 &[
3046 "v project_root",
3047 " > .git",
3048 " v dir_1",
3049 " > gitignored_dir",
3050 " file_1.py",
3051 " file_2.py",
3052 " file_3.py",
3053 " v dir_2",
3054 " file_1.py <== selected <== marked",
3055 " file_2.py",
3056 " file_3.py",
3057 " .gitignore",
3058 ],
3059 "When auto reveal is enabled, not ignored dir_2 entry should be revealed"
3060 );
3061
3062 panel.update(cx, |panel, cx| {
3063 panel.project.update(cx, |_, cx| {
3064 cx.emit(project::Event::ActiveEntryChanged(Some(
3065 gitignored_dir_file,
3066 )))
3067 })
3068 });
3069 cx.run_until_parked();
3070 assert_eq!(
3071 visible_entries_as_strings(&panel, 0..20, cx),
3072 &[
3073 "v project_root",
3074 " > .git",
3075 " v dir_1",
3076 " > gitignored_dir",
3077 " file_1.py",
3078 " file_2.py",
3079 " file_3.py",
3080 " v dir_2",
3081 " file_1.py <== selected <== marked",
3082 " file_2.py",
3083 " file_3.py",
3084 " .gitignore",
3085 ],
3086 "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
3087 );
3088
3089 panel.update(cx, |panel, cx| {
3090 panel.project.update(cx, |_, cx| {
3091 cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
3092 })
3093 });
3094 cx.run_until_parked();
3095 assert_eq!(
3096 visible_entries_as_strings(&panel, 0..20, cx),
3097 &[
3098 "v project_root",
3099 " > .git",
3100 " v dir_1",
3101 " v gitignored_dir",
3102 " file_a.py <== selected <== marked",
3103 " file_b.py",
3104 " file_c.py",
3105 " file_1.py",
3106 " file_2.py",
3107 " file_3.py",
3108 " v dir_2",
3109 " file_1.py",
3110 " file_2.py",
3111 " file_3.py",
3112 " .gitignore",
3113 ],
3114 "When a gitignored entry is explicitly revealed, it should be shown in the project tree"
3115 );
3116}
3117
3118#[gpui::test]
3119async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
3120 init_test_with_editor(cx);
3121 cx.update(|cx| {
3122 cx.update_global::<SettingsStore, _>(|store, cx| {
3123 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3124 worktree_settings.file_scan_exclusions = Some(Vec::new());
3125 worktree_settings.file_scan_inclusions =
3126 Some(vec!["always_included_but_ignored_dir/*".to_string()]);
3127 });
3128 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3129 project_panel_settings.auto_reveal_entries = Some(false)
3130 });
3131 })
3132 });
3133
3134 let fs = FakeFs::new(cx.background_executor.clone());
3135 fs.insert_tree(
3136 "/project_root",
3137 json!({
3138 ".git": {},
3139 ".gitignore": "**/gitignored_dir\n/always_included_but_ignored_dir",
3140 "dir_1": {
3141 "file_1.py": "# File 1_1 contents",
3142 "file_2.py": "# File 1_2 contents",
3143 "file_3.py": "# File 1_3 contents",
3144 "gitignored_dir": {
3145 "file_a.py": "# File contents",
3146 "file_b.py": "# File contents",
3147 "file_c.py": "# File contents",
3148 },
3149 },
3150 "dir_2": {
3151 "file_1.py": "# File 2_1 contents",
3152 "file_2.py": "# File 2_2 contents",
3153 "file_3.py": "# File 2_3 contents",
3154 },
3155 "always_included_but_ignored_dir": {
3156 "file_a.py": "# File contents",
3157 "file_b.py": "# File contents",
3158 "file_c.py": "# File contents",
3159 },
3160 }),
3161 )
3162 .await;
3163
3164 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3165 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3166 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3167 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3168
3169 assert_eq!(
3170 visible_entries_as_strings(&panel, 0..20, cx),
3171 &[
3172 "v project_root",
3173 " > .git",
3174 " > always_included_but_ignored_dir",
3175 " > dir_1",
3176 " > dir_2",
3177 " .gitignore",
3178 ]
3179 );
3180
3181 let gitignored_dir_file =
3182 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3183 let always_included_but_ignored_dir_file = find_project_entry(
3184 &panel,
3185 "project_root/always_included_but_ignored_dir/file_a.py",
3186 cx,
3187 )
3188 .expect("file that is .gitignored but set to always be included should have an entry");
3189 assert_eq!(
3190 gitignored_dir_file, None,
3191 "File in the gitignored dir should not have an entry unless its directory is toggled"
3192 );
3193
3194 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3195 cx.run_until_parked();
3196 cx.update(|_, cx| {
3197 cx.update_global::<SettingsStore, _>(|store, cx| {
3198 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3199 project_panel_settings.auto_reveal_entries = Some(true)
3200 });
3201 })
3202 });
3203
3204 panel.update(cx, |panel, cx| {
3205 panel.project.update(cx, |_, cx| {
3206 cx.emit(project::Event::ActiveEntryChanged(Some(
3207 always_included_but_ignored_dir_file,
3208 )))
3209 })
3210 });
3211 cx.run_until_parked();
3212
3213 assert_eq!(
3214 visible_entries_as_strings(&panel, 0..20, cx),
3215 &[
3216 "v project_root",
3217 " > .git",
3218 " v always_included_but_ignored_dir",
3219 " file_a.py <== selected <== marked",
3220 " file_b.py",
3221 " file_c.py",
3222 " v dir_1",
3223 " > gitignored_dir",
3224 " file_1.py",
3225 " file_2.py",
3226 " file_3.py",
3227 " > dir_2",
3228 " .gitignore",
3229 ],
3230 "When auto reveal is enabled, a gitignored but always included selected entry should be revealed in the project panel"
3231 );
3232}
3233
3234#[gpui::test]
3235async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
3236 init_test_with_editor(cx);
3237 cx.update(|cx| {
3238 cx.update_global::<SettingsStore, _>(|store, cx| {
3239 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3240 worktree_settings.file_scan_exclusions = Some(Vec::new());
3241 });
3242 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3243 project_panel_settings.auto_reveal_entries = Some(false)
3244 });
3245 })
3246 });
3247
3248 let fs = FakeFs::new(cx.background_executor.clone());
3249 fs.insert_tree(
3250 "/project_root",
3251 json!({
3252 ".git": {},
3253 ".gitignore": "**/gitignored_dir",
3254 "dir_1": {
3255 "file_1.py": "# File 1_1 contents",
3256 "file_2.py": "# File 1_2 contents",
3257 "file_3.py": "# File 1_3 contents",
3258 "gitignored_dir": {
3259 "file_a.py": "# File contents",
3260 "file_b.py": "# File contents",
3261 "file_c.py": "# File contents",
3262 },
3263 },
3264 "dir_2": {
3265 "file_1.py": "# File 2_1 contents",
3266 "file_2.py": "# File 2_2 contents",
3267 "file_3.py": "# File 2_3 contents",
3268 }
3269 }),
3270 )
3271 .await;
3272
3273 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3274 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3275 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3276 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3277
3278 assert_eq!(
3279 visible_entries_as_strings(&panel, 0..20, cx),
3280 &[
3281 "v project_root",
3282 " > .git",
3283 " > dir_1",
3284 " > dir_2",
3285 " .gitignore",
3286 ]
3287 );
3288
3289 let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
3290 .expect("dir 1 file is not ignored and should have an entry");
3291 let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
3292 .expect("dir 2 file is not ignored and should have an entry");
3293 let gitignored_dir_file =
3294 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3295 assert_eq!(
3296 gitignored_dir_file, None,
3297 "File in the gitignored dir should not have an entry before its dir is toggled"
3298 );
3299
3300 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3301 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3302 cx.run_until_parked();
3303 assert_eq!(
3304 visible_entries_as_strings(&panel, 0..20, cx),
3305 &[
3306 "v project_root",
3307 " > .git",
3308 " v dir_1",
3309 " v gitignored_dir <== selected",
3310 " file_a.py",
3311 " file_b.py",
3312 " file_c.py",
3313 " file_1.py",
3314 " file_2.py",
3315 " file_3.py",
3316 " > dir_2",
3317 " .gitignore",
3318 ],
3319 "Should show gitignored dir file list in the project panel"
3320 );
3321 let gitignored_dir_file =
3322 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
3323 .expect("after gitignored dir got opened, a file entry should be present");
3324
3325 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3326 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3327 assert_eq!(
3328 visible_entries_as_strings(&panel, 0..20, cx),
3329 &[
3330 "v project_root",
3331 " > .git",
3332 " > dir_1 <== selected",
3333 " > dir_2",
3334 " .gitignore",
3335 ],
3336 "Should hide all dir contents again and prepare for the explicit reveal test"
3337 );
3338
3339 for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
3340 panel.update(cx, |panel, cx| {
3341 panel.project.update(cx, |_, cx| {
3342 cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
3343 })
3344 });
3345 cx.run_until_parked();
3346 assert_eq!(
3347 visible_entries_as_strings(&panel, 0..20, cx),
3348 &[
3349 "v project_root",
3350 " > .git",
3351 " > dir_1 <== selected",
3352 " > dir_2",
3353 " .gitignore",
3354 ],
3355 "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
3356 );
3357 }
3358
3359 panel.update(cx, |panel, cx| {
3360 panel.project.update(cx, |_, cx| {
3361 cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
3362 })
3363 });
3364 cx.run_until_parked();
3365 assert_eq!(
3366 visible_entries_as_strings(&panel, 0..20, cx),
3367 &[
3368 "v project_root",
3369 " > .git",
3370 " v dir_1",
3371 " > gitignored_dir",
3372 " file_1.py <== selected <== marked",
3373 " file_2.py",
3374 " file_3.py",
3375 " > dir_2",
3376 " .gitignore",
3377 ],
3378 "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
3379 );
3380
3381 panel.update(cx, |panel, cx| {
3382 panel.project.update(cx, |_, cx| {
3383 cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
3384 })
3385 });
3386 cx.run_until_parked();
3387 assert_eq!(
3388 visible_entries_as_strings(&panel, 0..20, cx),
3389 &[
3390 "v project_root",
3391 " > .git",
3392 " v dir_1",
3393 " > gitignored_dir",
3394 " file_1.py",
3395 " file_2.py",
3396 " file_3.py",
3397 " v dir_2",
3398 " file_1.py <== selected <== marked",
3399 " file_2.py",
3400 " file_3.py",
3401 " .gitignore",
3402 ],
3403 "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
3404 );
3405
3406 panel.update(cx, |panel, cx| {
3407 panel.project.update(cx, |_, cx| {
3408 cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
3409 })
3410 });
3411 cx.run_until_parked();
3412 assert_eq!(
3413 visible_entries_as_strings(&panel, 0..20, cx),
3414 &[
3415 "v project_root",
3416 " > .git",
3417 " v dir_1",
3418 " v gitignored_dir",
3419 " file_a.py <== selected <== marked",
3420 " file_b.py",
3421 " file_c.py",
3422 " file_1.py",
3423 " file_2.py",
3424 " file_3.py",
3425 " v dir_2",
3426 " file_1.py",
3427 " file_2.py",
3428 " file_3.py",
3429 " .gitignore",
3430 ],
3431 "With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
3432 );
3433}
3434
3435#[gpui::test]
3436async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
3437 init_test(cx);
3438 cx.update(|cx| {
3439 cx.update_global::<SettingsStore, _>(|store, cx| {
3440 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
3441 project_settings.file_scan_exclusions =
3442 Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
3443 });
3444 });
3445 });
3446
3447 cx.update(|cx| {
3448 register_project_item::<TestProjectItemView>(cx);
3449 });
3450
3451 let fs = FakeFs::new(cx.executor().clone());
3452 fs.insert_tree(
3453 "/root1",
3454 json!({
3455 ".dockerignore": "",
3456 ".git": {
3457 "HEAD": "",
3458 },
3459 }),
3460 )
3461 .await;
3462
3463 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
3464 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3465 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3466 let panel = workspace
3467 .update(cx, |workspace, window, cx| {
3468 let panel = ProjectPanel::new(workspace, window, cx);
3469 workspace.add_panel(panel.clone(), window, cx);
3470 panel
3471 })
3472 .unwrap();
3473
3474 select_path(&panel, "root1", cx);
3475 assert_eq!(
3476 visible_entries_as_strings(&panel, 0..10, cx),
3477 &["v root1 <== selected", " .dockerignore",]
3478 );
3479 workspace
3480 .update(cx, |workspace, _, cx| {
3481 assert!(
3482 workspace.active_item(cx).is_none(),
3483 "Should have no active items in the beginning"
3484 );
3485 })
3486 .unwrap();
3487
3488 let excluded_file_path = ".git/COMMIT_EDITMSG";
3489 let excluded_dir_path = "excluded_dir";
3490
3491 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
3492 panel.update_in(cx, |panel, window, cx| {
3493 assert!(panel.filename_editor.read(cx).is_focused(window));
3494 });
3495 panel
3496 .update_in(cx, |panel, window, cx| {
3497 panel.filename_editor.update(cx, |editor, cx| {
3498 editor.set_text(excluded_file_path, window, cx)
3499 });
3500 panel.confirm_edit(window, cx).unwrap()
3501 })
3502 .await
3503 .unwrap();
3504
3505 assert_eq!(
3506 visible_entries_as_strings(&panel, 0..13, cx),
3507 &["v root1", " .dockerignore"],
3508 "Excluded dir should not be shown after opening a file in it"
3509 );
3510 panel.update_in(cx, |panel, window, cx| {
3511 assert!(
3512 !panel.filename_editor.read(cx).is_focused(window),
3513 "Should have closed the file name editor"
3514 );
3515 });
3516 workspace
3517 .update(cx, |workspace, _, cx| {
3518 let active_entry_path = workspace
3519 .active_item(cx)
3520 .expect("should have opened and activated the excluded item")
3521 .act_as::<TestProjectItemView>(cx)
3522 .expect("should have opened the corresponding project item for the excluded item")
3523 .read(cx)
3524 .path
3525 .clone();
3526 assert_eq!(
3527 active_entry_path.path.as_ref(),
3528 Path::new(excluded_file_path),
3529 "Should open the excluded file"
3530 );
3531
3532 assert!(
3533 workspace.notification_ids().is_empty(),
3534 "Should have no notifications after opening an excluded file"
3535 );
3536 })
3537 .unwrap();
3538 assert!(
3539 fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
3540 "Should have created the excluded file"
3541 );
3542
3543 select_path(&panel, "root1", cx);
3544 panel.update_in(cx, |panel, window, cx| {
3545 panel.new_directory(&NewDirectory, window, cx)
3546 });
3547 panel.update_in(cx, |panel, window, cx| {
3548 assert!(panel.filename_editor.read(cx).is_focused(window));
3549 });
3550 panel
3551 .update_in(cx, |panel, window, cx| {
3552 panel.filename_editor.update(cx, |editor, cx| {
3553 editor.set_text(excluded_file_path, window, cx)
3554 });
3555 panel.confirm_edit(window, cx).unwrap()
3556 })
3557 .await
3558 .unwrap();
3559
3560 assert_eq!(
3561 visible_entries_as_strings(&panel, 0..13, cx),
3562 &["v root1", " .dockerignore"],
3563 "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
3564 );
3565 panel.update_in(cx, |panel, window, cx| {
3566 assert!(
3567 !panel.filename_editor.read(cx).is_focused(window),
3568 "Should have closed the file name editor"
3569 );
3570 });
3571 workspace
3572 .update(cx, |workspace, _, cx| {
3573 let notifications = workspace.notification_ids();
3574 assert_eq!(
3575 notifications.len(),
3576 1,
3577 "Should receive one notification with the error message"
3578 );
3579 workspace.dismiss_notification(notifications.first().unwrap(), cx);
3580 assert!(workspace.notification_ids().is_empty());
3581 })
3582 .unwrap();
3583
3584 select_path(&panel, "root1", cx);
3585 panel.update_in(cx, |panel, window, cx| {
3586 panel.new_directory(&NewDirectory, window, cx)
3587 });
3588 panel.update_in(cx, |panel, window, cx| {
3589 assert!(panel.filename_editor.read(cx).is_focused(window));
3590 });
3591 panel
3592 .update_in(cx, |panel, window, cx| {
3593 panel.filename_editor.update(cx, |editor, cx| {
3594 editor.set_text(excluded_dir_path, window, cx)
3595 });
3596 panel.confirm_edit(window, cx).unwrap()
3597 })
3598 .await
3599 .unwrap();
3600
3601 assert_eq!(
3602 visible_entries_as_strings(&panel, 0..13, cx),
3603 &["v root1", " .dockerignore"],
3604 "Should not change the project panel after trying to create an excluded directory"
3605 );
3606 panel.update_in(cx, |panel, window, cx| {
3607 assert!(
3608 !panel.filename_editor.read(cx).is_focused(window),
3609 "Should have closed the file name editor"
3610 );
3611 });
3612 workspace
3613 .update(cx, |workspace, _, cx| {
3614 let notifications = workspace.notification_ids();
3615 assert_eq!(
3616 notifications.len(),
3617 1,
3618 "Should receive one notification explaining that no directory is actually shown"
3619 );
3620 workspace.dismiss_notification(notifications.first().unwrap(), cx);
3621 assert!(workspace.notification_ids().is_empty());
3622 })
3623 .unwrap();
3624 assert!(
3625 fs.is_dir(Path::new("/root1/excluded_dir")).await,
3626 "Should have created the excluded directory"
3627 );
3628}
3629
3630#[gpui::test]
3631async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) {
3632 init_test_with_editor(cx);
3633
3634 let fs = FakeFs::new(cx.executor().clone());
3635 fs.insert_tree(
3636 "/src",
3637 json!({
3638 "test": {
3639 "first.rs": "// First Rust file",
3640 "second.rs": "// Second Rust file",
3641 "third.rs": "// Third Rust file",
3642 }
3643 }),
3644 )
3645 .await;
3646
3647 let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
3648 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3649 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3650 let panel = workspace
3651 .update(cx, |workspace, window, cx| {
3652 let panel = ProjectPanel::new(workspace, window, cx);
3653 workspace.add_panel(panel.clone(), window, cx);
3654 panel
3655 })
3656 .unwrap();
3657
3658 select_path(&panel, "src/", cx);
3659 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
3660 cx.executor().run_until_parked();
3661 assert_eq!(
3662 visible_entries_as_strings(&panel, 0..10, cx),
3663 &[
3664 //
3665 "v src <== selected",
3666 " > test"
3667 ]
3668 );
3669 panel.update_in(cx, |panel, window, cx| {
3670 panel.new_directory(&NewDirectory, window, cx)
3671 });
3672 panel.update_in(cx, |panel, window, cx| {
3673 assert!(panel.filename_editor.read(cx).is_focused(window));
3674 });
3675 assert_eq!(
3676 visible_entries_as_strings(&panel, 0..10, cx),
3677 &[
3678 //
3679 "v src",
3680 " > [EDITOR: ''] <== selected",
3681 " > test"
3682 ]
3683 );
3684
3685 panel.update_in(cx, |panel, window, cx| {
3686 panel.cancel(&menu::Cancel, window, cx)
3687 });
3688 assert_eq!(
3689 visible_entries_as_strings(&panel, 0..10, cx),
3690 &[
3691 //
3692 "v src <== selected",
3693 " > test"
3694 ]
3695 );
3696}
3697
3698#[gpui::test]
3699async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
3700 init_test_with_editor(cx);
3701
3702 let fs = FakeFs::new(cx.executor().clone());
3703 fs.insert_tree(
3704 "/root",
3705 json!({
3706 "dir1": {
3707 "subdir1": {},
3708 "file1.txt": "",
3709 "file2.txt": "",
3710 },
3711 "dir2": {
3712 "subdir2": {},
3713 "file3.txt": "",
3714 "file4.txt": "",
3715 },
3716 "file5.txt": "",
3717 "file6.txt": "",
3718 }),
3719 )
3720 .await;
3721
3722 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
3723 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3724 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3725 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3726
3727 toggle_expand_dir(&panel, "root/dir1", cx);
3728 toggle_expand_dir(&panel, "root/dir2", cx);
3729
3730 // Test Case 1: Delete middle file in directory
3731 select_path(&panel, "root/dir1/file1.txt", cx);
3732 assert_eq!(
3733 visible_entries_as_strings(&panel, 0..15, cx),
3734 &[
3735 "v root",
3736 " v dir1",
3737 " > subdir1",
3738 " file1.txt <== selected",
3739 " file2.txt",
3740 " v dir2",
3741 " > subdir2",
3742 " file3.txt",
3743 " file4.txt",
3744 " file5.txt",
3745 " file6.txt",
3746 ],
3747 "Initial state before deleting middle file"
3748 );
3749
3750 submit_deletion(&panel, cx);
3751 assert_eq!(
3752 visible_entries_as_strings(&panel, 0..15, cx),
3753 &[
3754 "v root",
3755 " v dir1",
3756 " > subdir1",
3757 " file2.txt <== selected",
3758 " v dir2",
3759 " > subdir2",
3760 " file3.txt",
3761 " file4.txt",
3762 " file5.txt",
3763 " file6.txt",
3764 ],
3765 "Should select next file after deleting middle file"
3766 );
3767
3768 // Test Case 2: Delete last file in directory
3769 submit_deletion(&panel, cx);
3770 assert_eq!(
3771 visible_entries_as_strings(&panel, 0..15, cx),
3772 &[
3773 "v root",
3774 " v dir1",
3775 " > subdir1 <== selected",
3776 " v dir2",
3777 " > subdir2",
3778 " file3.txt",
3779 " file4.txt",
3780 " file5.txt",
3781 " file6.txt",
3782 ],
3783 "Should select next directory when last file is deleted"
3784 );
3785
3786 // Test Case 3: Delete root level file
3787 select_path(&panel, "root/file6.txt", cx);
3788 assert_eq!(
3789 visible_entries_as_strings(&panel, 0..15, cx),
3790 &[
3791 "v root",
3792 " v dir1",
3793 " > subdir1",
3794 " v dir2",
3795 " > subdir2",
3796 " file3.txt",
3797 " file4.txt",
3798 " file5.txt",
3799 " file6.txt <== selected",
3800 ],
3801 "Initial state before deleting root level file"
3802 );
3803
3804 submit_deletion(&panel, cx);
3805 assert_eq!(
3806 visible_entries_as_strings(&panel, 0..15, cx),
3807 &[
3808 "v root",
3809 " v dir1",
3810 " > subdir1",
3811 " v dir2",
3812 " > subdir2",
3813 " file3.txt",
3814 " file4.txt",
3815 " file5.txt <== selected",
3816 ],
3817 "Should select prev entry at root level"
3818 );
3819}
3820
3821#[gpui::test]
3822async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) {
3823 init_test_with_editor(cx);
3824
3825 let fs = FakeFs::new(cx.executor().clone());
3826 fs.insert_tree(
3827 path!("/root"),
3828 json!({
3829 "aa": "// Testing 1",
3830 "bb": "// Testing 2",
3831 "cc": "// Testing 3",
3832 "dd": "// Testing 4",
3833 "ee": "// Testing 5",
3834 "ff": "// Testing 6",
3835 "gg": "// Testing 7",
3836 "hh": "// Testing 8",
3837 "ii": "// Testing 8",
3838 ".gitignore": "bb\ndd\nee\nff\nii\n'",
3839 }),
3840 )
3841 .await;
3842
3843 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3844 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3845 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3846
3847 // Test 1: Auto selection with one gitignored file next to the deleted file
3848 cx.update(|_, cx| {
3849 let settings = *ProjectPanelSettings::get_global(cx);
3850 ProjectPanelSettings::override_global(
3851 ProjectPanelSettings {
3852 hide_gitignore: true,
3853 ..settings
3854 },
3855 cx,
3856 );
3857 });
3858
3859 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3860
3861 select_path(&panel, "root/aa", cx);
3862 assert_eq!(
3863 visible_entries_as_strings(&panel, 0..10, cx),
3864 &[
3865 "v root",
3866 " .gitignore",
3867 " aa <== selected",
3868 " cc",
3869 " gg",
3870 " hh"
3871 ],
3872 "Initial state should hide files on .gitignore"
3873 );
3874
3875 submit_deletion(&panel, cx);
3876
3877 assert_eq!(
3878 visible_entries_as_strings(&panel, 0..10, cx),
3879 &[
3880 "v root",
3881 " .gitignore",
3882 " cc <== selected",
3883 " gg",
3884 " hh"
3885 ],
3886 "Should select next entry not on .gitignore"
3887 );
3888
3889 // Test 2: Auto selection with many gitignored files next to the deleted file
3890 submit_deletion(&panel, cx);
3891 assert_eq!(
3892 visible_entries_as_strings(&panel, 0..10, cx),
3893 &[
3894 "v root",
3895 " .gitignore",
3896 " gg <== selected",
3897 " hh"
3898 ],
3899 "Should select next entry not on .gitignore"
3900 );
3901
3902 // Test 3: Auto selection of entry before deleted file
3903 select_path(&panel, "root/hh", cx);
3904 assert_eq!(
3905 visible_entries_as_strings(&panel, 0..10, cx),
3906 &[
3907 "v root",
3908 " .gitignore",
3909 " gg",
3910 " hh <== selected"
3911 ],
3912 "Should select next entry not on .gitignore"
3913 );
3914 submit_deletion(&panel, cx);
3915 assert_eq!(
3916 visible_entries_as_strings(&panel, 0..10, cx),
3917 &["v root", " .gitignore", " gg <== selected"],
3918 "Should select next entry not on .gitignore"
3919 );
3920}
3921
3922#[gpui::test]
3923async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) {
3924 init_test_with_editor(cx);
3925
3926 let fs = FakeFs::new(cx.executor().clone());
3927 fs.insert_tree(
3928 path!("/root"),
3929 json!({
3930 "dir1": {
3931 "file1": "// Testing",
3932 "file2": "// Testing",
3933 "file3": "// Testing"
3934 },
3935 "aa": "// Testing",
3936 ".gitignore": "file1\nfile3\n",
3937 }),
3938 )
3939 .await;
3940
3941 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3942 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3943 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3944
3945 cx.update(|_, cx| {
3946 let settings = *ProjectPanelSettings::get_global(cx);
3947 ProjectPanelSettings::override_global(
3948 ProjectPanelSettings {
3949 hide_gitignore: true,
3950 ..settings
3951 },
3952 cx,
3953 );
3954 });
3955
3956 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3957
3958 // Test 1: Visible items should exclude files on gitignore
3959 toggle_expand_dir(&panel, "root/dir1", cx);
3960 select_path(&panel, "root/dir1/file2", cx);
3961 assert_eq!(
3962 visible_entries_as_strings(&panel, 0..10, cx),
3963 &[
3964 "v root",
3965 " v dir1",
3966 " file2 <== selected",
3967 " .gitignore",
3968 " aa"
3969 ],
3970 "Initial state should hide files on .gitignore"
3971 );
3972 submit_deletion(&panel, cx);
3973
3974 // Test 2: Auto selection should go to the parent
3975 assert_eq!(
3976 visible_entries_as_strings(&panel, 0..10, cx),
3977 &[
3978 "v root",
3979 " v dir1 <== selected",
3980 " .gitignore",
3981 " aa"
3982 ],
3983 "Initial state should hide files on .gitignore"
3984 );
3985}
3986
3987#[gpui::test]
3988async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
3989 init_test_with_editor(cx);
3990
3991 let fs = FakeFs::new(cx.executor().clone());
3992 fs.insert_tree(
3993 "/root",
3994 json!({
3995 "dir1": {
3996 "subdir1": {
3997 "a.txt": "",
3998 "b.txt": ""
3999 },
4000 "file1.txt": "",
4001 },
4002 "dir2": {
4003 "subdir2": {
4004 "c.txt": "",
4005 "d.txt": ""
4006 },
4007 "file2.txt": "",
4008 },
4009 "file3.txt": "",
4010 }),
4011 )
4012 .await;
4013
4014 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4015 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4016 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4017 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4018
4019 toggle_expand_dir(&panel, "root/dir1", cx);
4020 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4021 toggle_expand_dir(&panel, "root/dir2", cx);
4022 toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
4023
4024 // Test Case 1: Select and delete nested directory with parent
4025 cx.simulate_modifiers_change(gpui::Modifiers {
4026 control: true,
4027 ..Default::default()
4028 });
4029 select_path_with_mark(&panel, "root/dir1/subdir1", cx);
4030 select_path_with_mark(&panel, "root/dir1", cx);
4031
4032 assert_eq!(
4033 visible_entries_as_strings(&panel, 0..15, cx),
4034 &[
4035 "v root",
4036 " v dir1 <== selected <== marked",
4037 " v subdir1 <== marked",
4038 " a.txt",
4039 " b.txt",
4040 " file1.txt",
4041 " v dir2",
4042 " v subdir2",
4043 " c.txt",
4044 " d.txt",
4045 " file2.txt",
4046 " file3.txt",
4047 ],
4048 "Initial state before deleting nested directory with parent"
4049 );
4050
4051 submit_deletion(&panel, cx);
4052 assert_eq!(
4053 visible_entries_as_strings(&panel, 0..15, cx),
4054 &[
4055 "v root",
4056 " v dir2 <== selected",
4057 " v subdir2",
4058 " c.txt",
4059 " d.txt",
4060 " file2.txt",
4061 " file3.txt",
4062 ],
4063 "Should select next directory after deleting directory with parent"
4064 );
4065
4066 // Test Case 2: Select mixed files and directories across levels
4067 select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx);
4068 select_path_with_mark(&panel, "root/dir2/file2.txt", cx);
4069 select_path_with_mark(&panel, "root/file3.txt", cx);
4070
4071 assert_eq!(
4072 visible_entries_as_strings(&panel, 0..15, cx),
4073 &[
4074 "v root",
4075 " v dir2",
4076 " v subdir2",
4077 " c.txt <== marked",
4078 " d.txt",
4079 " file2.txt <== marked",
4080 " file3.txt <== selected <== marked",
4081 ],
4082 "Initial state before deleting"
4083 );
4084
4085 submit_deletion(&panel, cx);
4086 assert_eq!(
4087 visible_entries_as_strings(&panel, 0..15, cx),
4088 &[
4089 "v root",
4090 " v dir2 <== selected",
4091 " v subdir2",
4092 " d.txt",
4093 ],
4094 "Should select sibling directory"
4095 );
4096}
4097
4098#[gpui::test]
4099async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) {
4100 init_test_with_editor(cx);
4101
4102 let fs = FakeFs::new(cx.executor().clone());
4103 fs.insert_tree(
4104 "/root",
4105 json!({
4106 "dir1": {
4107 "subdir1": {
4108 "a.txt": "",
4109 "b.txt": ""
4110 },
4111 "file1.txt": "",
4112 },
4113 "dir2": {
4114 "subdir2": {
4115 "c.txt": "",
4116 "d.txt": ""
4117 },
4118 "file2.txt": "",
4119 },
4120 "file3.txt": "",
4121 "file4.txt": "",
4122 }),
4123 )
4124 .await;
4125
4126 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4127 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4128 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4129 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4130
4131 toggle_expand_dir(&panel, "root/dir1", cx);
4132 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4133 toggle_expand_dir(&panel, "root/dir2", cx);
4134 toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
4135
4136 // Test Case 1: Select all root files and directories
4137 cx.simulate_modifiers_change(gpui::Modifiers {
4138 control: true,
4139 ..Default::default()
4140 });
4141 select_path_with_mark(&panel, "root/dir1", cx);
4142 select_path_with_mark(&panel, "root/dir2", cx);
4143 select_path_with_mark(&panel, "root/file3.txt", cx);
4144 select_path_with_mark(&panel, "root/file4.txt", cx);
4145 assert_eq!(
4146 visible_entries_as_strings(&panel, 0..20, cx),
4147 &[
4148 "v root",
4149 " v dir1 <== marked",
4150 " v subdir1",
4151 " a.txt",
4152 " b.txt",
4153 " file1.txt",
4154 " v dir2 <== marked",
4155 " v subdir2",
4156 " c.txt",
4157 " d.txt",
4158 " file2.txt",
4159 " file3.txt <== marked",
4160 " file4.txt <== selected <== marked",
4161 ],
4162 "State before deleting all contents"
4163 );
4164
4165 submit_deletion(&panel, cx);
4166 assert_eq!(
4167 visible_entries_as_strings(&panel, 0..20, cx),
4168 &["v root <== selected"],
4169 "Only empty root directory should remain after deleting all contents"
4170 );
4171}
4172
4173#[gpui::test]
4174async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) {
4175 init_test_with_editor(cx);
4176
4177 let fs = FakeFs::new(cx.executor().clone());
4178 fs.insert_tree(
4179 "/root",
4180 json!({
4181 "dir1": {
4182 "subdir1": {
4183 "file_a.txt": "content a",
4184 "file_b.txt": "content b",
4185 },
4186 "subdir2": {
4187 "file_c.txt": "content c",
4188 },
4189 "file1.txt": "content 1",
4190 },
4191 "dir2": {
4192 "file2.txt": "content 2",
4193 },
4194 }),
4195 )
4196 .await;
4197
4198 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4199 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4200 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4201 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4202
4203 toggle_expand_dir(&panel, "root/dir1", cx);
4204 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4205 toggle_expand_dir(&panel, "root/dir2", cx);
4206 cx.simulate_modifiers_change(gpui::Modifiers {
4207 control: true,
4208 ..Default::default()
4209 });
4210
4211 // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory
4212 select_path_with_mark(&panel, "root/dir1", cx);
4213 select_path_with_mark(&panel, "root/dir1/subdir1", cx);
4214 select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx);
4215
4216 assert_eq!(
4217 visible_entries_as_strings(&panel, 0..20, cx),
4218 &[
4219 "v root",
4220 " v dir1 <== marked",
4221 " v subdir1 <== marked",
4222 " file_a.txt <== selected <== marked",
4223 " file_b.txt",
4224 " > subdir2",
4225 " file1.txt",
4226 " v dir2",
4227 " file2.txt",
4228 ],
4229 "State with parent dir, subdir, and file selected"
4230 );
4231 submit_deletion(&panel, cx);
4232 assert_eq!(
4233 visible_entries_as_strings(&panel, 0..20, cx),
4234 &["v root", " v dir2 <== selected", " file2.txt",],
4235 "Only dir2 should remain after deletion"
4236 );
4237}
4238
4239#[gpui::test]
4240async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) {
4241 init_test_with_editor(cx);
4242
4243 let fs = FakeFs::new(cx.executor().clone());
4244 // First worktree
4245 fs.insert_tree(
4246 "/root1",
4247 json!({
4248 "dir1": {
4249 "file1.txt": "content 1",
4250 "file2.txt": "content 2",
4251 },
4252 "dir2": {
4253 "file3.txt": "content 3",
4254 },
4255 }),
4256 )
4257 .await;
4258
4259 // Second worktree
4260 fs.insert_tree(
4261 "/root2",
4262 json!({
4263 "dir3": {
4264 "file4.txt": "content 4",
4265 "file5.txt": "content 5",
4266 },
4267 "file6.txt": "content 6",
4268 }),
4269 )
4270 .await;
4271
4272 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
4273 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4274 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4275 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4276
4277 // Expand all directories for testing
4278 toggle_expand_dir(&panel, "root1/dir1", cx);
4279 toggle_expand_dir(&panel, "root1/dir2", cx);
4280 toggle_expand_dir(&panel, "root2/dir3", cx);
4281
4282 // Test Case 1: Delete files across different worktrees
4283 cx.simulate_modifiers_change(gpui::Modifiers {
4284 control: true,
4285 ..Default::default()
4286 });
4287 select_path_with_mark(&panel, "root1/dir1/file1.txt", cx);
4288 select_path_with_mark(&panel, "root2/dir3/file4.txt", cx);
4289
4290 assert_eq!(
4291 visible_entries_as_strings(&panel, 0..20, cx),
4292 &[
4293 "v root1",
4294 " v dir1",
4295 " file1.txt <== marked",
4296 " file2.txt",
4297 " v dir2",
4298 " file3.txt",
4299 "v root2",
4300 " v dir3",
4301 " file4.txt <== selected <== marked",
4302 " file5.txt",
4303 " file6.txt",
4304 ],
4305 "Initial state with files selected from different worktrees"
4306 );
4307
4308 submit_deletion(&panel, cx);
4309 assert_eq!(
4310 visible_entries_as_strings(&panel, 0..20, cx),
4311 &[
4312 "v root1",
4313 " v dir1",
4314 " file2.txt",
4315 " v dir2",
4316 " file3.txt",
4317 "v root2",
4318 " v dir3",
4319 " file5.txt <== selected",
4320 " file6.txt",
4321 ],
4322 "Should select next file in the last worktree after deletion"
4323 );
4324
4325 // Test Case 2: Delete directories from different worktrees
4326 select_path_with_mark(&panel, "root1/dir1", cx);
4327 select_path_with_mark(&panel, "root2/dir3", cx);
4328
4329 assert_eq!(
4330 visible_entries_as_strings(&panel, 0..20, cx),
4331 &[
4332 "v root1",
4333 " v dir1 <== marked",
4334 " file2.txt",
4335 " v dir2",
4336 " file3.txt",
4337 "v root2",
4338 " v dir3 <== selected <== marked",
4339 " file5.txt",
4340 " file6.txt",
4341 ],
4342 "State with directories marked from different worktrees"
4343 );
4344
4345 submit_deletion(&panel, cx);
4346 assert_eq!(
4347 visible_entries_as_strings(&panel, 0..20, cx),
4348 &[
4349 "v root1",
4350 " v dir2",
4351 " file3.txt",
4352 "v root2",
4353 " file6.txt <== selected",
4354 ],
4355 "Should select remaining file in last worktree after directory deletion"
4356 );
4357
4358 // Test Case 4: Delete all remaining files except roots
4359 select_path_with_mark(&panel, "root1/dir2/file3.txt", cx);
4360 select_path_with_mark(&panel, "root2/file6.txt", cx);
4361
4362 assert_eq!(
4363 visible_entries_as_strings(&panel, 0..20, cx),
4364 &[
4365 "v root1",
4366 " v dir2",
4367 " file3.txt <== marked",
4368 "v root2",
4369 " file6.txt <== selected <== marked",
4370 ],
4371 "State with all remaining files marked"
4372 );
4373
4374 submit_deletion(&panel, cx);
4375 assert_eq!(
4376 visible_entries_as_strings(&panel, 0..20, cx),
4377 &["v root1", " v dir2", "v root2 <== selected"],
4378 "Second parent root should be selected after deleting"
4379 );
4380}
4381
4382#[gpui::test]
4383async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) {
4384 init_test_with_editor(cx);
4385
4386 let fs = FakeFs::new(cx.executor().clone());
4387 fs.insert_tree(
4388 "/root",
4389 json!({
4390 "dir1": {
4391 "file1.txt": "",
4392 "file2.txt": "",
4393 "file3.txt": "",
4394 },
4395 "dir2": {
4396 "file4.txt": "",
4397 "file5.txt": "",
4398 },
4399 }),
4400 )
4401 .await;
4402
4403 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4404 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4405 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4406 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4407
4408 toggle_expand_dir(&panel, "root/dir1", cx);
4409 toggle_expand_dir(&panel, "root/dir2", cx);
4410
4411 cx.simulate_modifiers_change(gpui::Modifiers {
4412 control: true,
4413 ..Default::default()
4414 });
4415
4416 select_path_with_mark(&panel, "root/dir1/file2.txt", cx);
4417 select_path(&panel, "root/dir1/file1.txt", cx);
4418
4419 assert_eq!(
4420 visible_entries_as_strings(&panel, 0..15, cx),
4421 &[
4422 "v root",
4423 " v dir1",
4424 " file1.txt <== selected",
4425 " file2.txt <== marked",
4426 " file3.txt",
4427 " v dir2",
4428 " file4.txt",
4429 " file5.txt",
4430 ],
4431 "Initial state with one marked entry and different selection"
4432 );
4433
4434 // Delete should operate on the selected entry (file1.txt)
4435 submit_deletion(&panel, cx);
4436 assert_eq!(
4437 visible_entries_as_strings(&panel, 0..15, cx),
4438 &[
4439 "v root",
4440 " v dir1",
4441 " file2.txt <== selected <== marked",
4442 " file3.txt",
4443 " v dir2",
4444 " file4.txt",
4445 " file5.txt",
4446 ],
4447 "Should delete selected file, not marked file"
4448 );
4449
4450 select_path_with_mark(&panel, "root/dir1/file3.txt", cx);
4451 select_path_with_mark(&panel, "root/dir2/file4.txt", cx);
4452 select_path(&panel, "root/dir2/file5.txt", cx);
4453
4454 assert_eq!(
4455 visible_entries_as_strings(&panel, 0..15, cx),
4456 &[
4457 "v root",
4458 " v dir1",
4459 " file2.txt <== marked",
4460 " file3.txt <== marked",
4461 " v dir2",
4462 " file4.txt <== marked",
4463 " file5.txt <== selected",
4464 ],
4465 "Initial state with multiple marked entries and different selection"
4466 );
4467
4468 // Delete should operate on all marked entries, ignoring the selection
4469 submit_deletion(&panel, cx);
4470 assert_eq!(
4471 visible_entries_as_strings(&panel, 0..15, cx),
4472 &[
4473 "v root",
4474 " v dir1",
4475 " v dir2",
4476 " file5.txt <== selected",
4477 ],
4478 "Should delete all marked files, leaving only the selected file"
4479 );
4480}
4481
4482#[gpui::test]
4483async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) {
4484 init_test_with_editor(cx);
4485
4486 let fs = FakeFs::new(cx.executor().clone());
4487 fs.insert_tree(
4488 "/root_b",
4489 json!({
4490 "dir1": {
4491 "file1.txt": "content 1",
4492 "file2.txt": "content 2",
4493 },
4494 }),
4495 )
4496 .await;
4497
4498 fs.insert_tree(
4499 "/root_c",
4500 json!({
4501 "dir2": {},
4502 }),
4503 )
4504 .await;
4505
4506 let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await;
4507 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4508 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4509 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4510
4511 toggle_expand_dir(&panel, "root_b/dir1", cx);
4512 toggle_expand_dir(&panel, "root_c/dir2", cx);
4513
4514 cx.simulate_modifiers_change(gpui::Modifiers {
4515 control: true,
4516 ..Default::default()
4517 });
4518 select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx);
4519 select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx);
4520
4521 assert_eq!(
4522 visible_entries_as_strings(&panel, 0..20, cx),
4523 &[
4524 "v root_b",
4525 " v dir1",
4526 " file1.txt <== marked",
4527 " file2.txt <== selected <== marked",
4528 "v root_c",
4529 " v dir2",
4530 ],
4531 "Initial state with files marked in root_b"
4532 );
4533
4534 submit_deletion(&panel, cx);
4535 assert_eq!(
4536 visible_entries_as_strings(&panel, 0..20, cx),
4537 &[
4538 "v root_b",
4539 " v dir1 <== selected",
4540 "v root_c",
4541 " v dir2",
4542 ],
4543 "After deletion in root_b as it's last deletion, selection should be in root_b"
4544 );
4545
4546 select_path_with_mark(&panel, "root_c/dir2", cx);
4547
4548 submit_deletion(&panel, cx);
4549 assert_eq!(
4550 visible_entries_as_strings(&panel, 0..20, cx),
4551 &["v root_b", " v dir1", "v root_c <== selected",],
4552 "After deleting from root_c, it should remain in root_c"
4553 );
4554}
4555
4556fn toggle_expand_dir(
4557 panel: &Entity<ProjectPanel>,
4558 path: impl AsRef<Path>,
4559 cx: &mut VisualTestContext,
4560) {
4561 let path = path.as_ref();
4562 panel.update_in(cx, |panel, window, cx| {
4563 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4564 let worktree = worktree.read(cx);
4565 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4566 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
4567 panel.toggle_expanded(entry_id, window, cx);
4568 return;
4569 }
4570 }
4571 panic!("no worktree for path {:?}", path);
4572 });
4573}
4574
4575#[gpui::test]
4576async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) {
4577 init_test_with_editor(cx);
4578
4579 let fs = FakeFs::new(cx.executor().clone());
4580 fs.insert_tree(
4581 path!("/root"),
4582 json!({
4583 ".gitignore": "**/ignored_dir\n**/ignored_nested",
4584 "dir1": {
4585 "empty1": {
4586 "empty2": {
4587 "empty3": {
4588 "file.txt": ""
4589 }
4590 }
4591 },
4592 "subdir1": {
4593 "file1.txt": "",
4594 "file2.txt": "",
4595 "ignored_nested": {
4596 "ignored_file.txt": ""
4597 }
4598 },
4599 "ignored_dir": {
4600 "subdir": {
4601 "deep_file.txt": ""
4602 }
4603 }
4604 }
4605 }),
4606 )
4607 .await;
4608
4609 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4610 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4611 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4612
4613 // Test 1: When auto-fold is enabled
4614 cx.update(|_, cx| {
4615 let settings = *ProjectPanelSettings::get_global(cx);
4616 ProjectPanelSettings::override_global(
4617 ProjectPanelSettings {
4618 auto_fold_dirs: true,
4619 ..settings
4620 },
4621 cx,
4622 );
4623 });
4624
4625 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4626
4627 assert_eq!(
4628 visible_entries_as_strings(&panel, 0..20, cx),
4629 &["v root", " > dir1", " .gitignore",],
4630 "Initial state should show collapsed root structure"
4631 );
4632
4633 toggle_expand_dir(&panel, "root/dir1", cx);
4634 assert_eq!(
4635 visible_entries_as_strings(&panel, 0..20, cx),
4636 &[
4637 separator!("v root"),
4638 separator!(" v dir1 <== selected"),
4639 separator!(" > empty1/empty2/empty3"),
4640 separator!(" > ignored_dir"),
4641 separator!(" > subdir1"),
4642 separator!(" .gitignore"),
4643 ],
4644 "Should show first level with auto-folded dirs and ignored dir visible"
4645 );
4646
4647 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4648 panel.update(cx, |panel, cx| {
4649 let project = panel.project.read(cx);
4650 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4651 panel.expand_all_for_entry(worktree.id(), entry_id, cx);
4652 panel.update_visible_entries(None, cx);
4653 });
4654 cx.run_until_parked();
4655
4656 assert_eq!(
4657 visible_entries_as_strings(&panel, 0..20, cx),
4658 &[
4659 separator!("v root"),
4660 separator!(" v dir1 <== selected"),
4661 separator!(" v empty1"),
4662 separator!(" v empty2"),
4663 separator!(" v empty3"),
4664 separator!(" file.txt"),
4665 separator!(" > ignored_dir"),
4666 separator!(" v subdir1"),
4667 separator!(" > ignored_nested"),
4668 separator!(" file1.txt"),
4669 separator!(" file2.txt"),
4670 separator!(" .gitignore"),
4671 ],
4672 "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested"
4673 );
4674
4675 // Test 2: When auto-fold is disabled
4676 cx.update(|_, cx| {
4677 let settings = *ProjectPanelSettings::get_global(cx);
4678 ProjectPanelSettings::override_global(
4679 ProjectPanelSettings {
4680 auto_fold_dirs: false,
4681 ..settings
4682 },
4683 cx,
4684 );
4685 });
4686
4687 panel.update_in(cx, |panel, window, cx| {
4688 panel.collapse_all_entries(&CollapseAllEntries, window, cx);
4689 });
4690
4691 toggle_expand_dir(&panel, "root/dir1", cx);
4692 assert_eq!(
4693 visible_entries_as_strings(&panel, 0..20, cx),
4694 &[
4695 separator!("v root"),
4696 separator!(" v dir1 <== selected"),
4697 separator!(" > empty1"),
4698 separator!(" > ignored_dir"),
4699 separator!(" > subdir1"),
4700 separator!(" .gitignore"),
4701 ],
4702 "With auto-fold disabled: should show all directories separately"
4703 );
4704
4705 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4706 panel.update(cx, |panel, cx| {
4707 let project = panel.project.read(cx);
4708 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4709 panel.expand_all_for_entry(worktree.id(), entry_id, cx);
4710 panel.update_visible_entries(None, cx);
4711 });
4712 cx.run_until_parked();
4713
4714 assert_eq!(
4715 visible_entries_as_strings(&panel, 0..20, cx),
4716 &[
4717 separator!("v root"),
4718 separator!(" v dir1 <== selected"),
4719 separator!(" v empty1"),
4720 separator!(" v empty2"),
4721 separator!(" v empty3"),
4722 separator!(" file.txt"),
4723 separator!(" > ignored_dir"),
4724 separator!(" v subdir1"),
4725 separator!(" > ignored_nested"),
4726 separator!(" file1.txt"),
4727 separator!(" file2.txt"),
4728 separator!(" .gitignore"),
4729 ],
4730 "After expand_all without auto-fold: should expand all dirs normally, \
4731 expand ignored_dir itself but not its subdirs, and not expand ignored_nested"
4732 );
4733
4734 // Test 3: When explicitly called on ignored directory
4735 let ignored_dir_entry = find_project_entry(&panel, "root/dir1/ignored_dir", cx).unwrap();
4736 panel.update(cx, |panel, cx| {
4737 let project = panel.project.read(cx);
4738 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4739 panel.expand_all_for_entry(worktree.id(), ignored_dir_entry, cx);
4740 panel.update_visible_entries(None, cx);
4741 });
4742 cx.run_until_parked();
4743
4744 assert_eq!(
4745 visible_entries_as_strings(&panel, 0..20, cx),
4746 &[
4747 separator!("v root"),
4748 separator!(" v dir1 <== selected"),
4749 separator!(" v empty1"),
4750 separator!(" v empty2"),
4751 separator!(" v empty3"),
4752 separator!(" file.txt"),
4753 separator!(" v ignored_dir"),
4754 separator!(" v subdir"),
4755 separator!(" deep_file.txt"),
4756 separator!(" v subdir1"),
4757 separator!(" > ignored_nested"),
4758 separator!(" file1.txt"),
4759 separator!(" file2.txt"),
4760 separator!(" .gitignore"),
4761 ],
4762 "After expand_all on ignored_dir: should expand all contents of the ignored directory"
4763 );
4764}
4765
4766#[gpui::test]
4767async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) {
4768 init_test(cx);
4769
4770 let fs = FakeFs::new(cx.executor().clone());
4771 fs.insert_tree(
4772 path!("/root"),
4773 json!({
4774 "dir1": {
4775 "subdir1": {
4776 "nested1": {
4777 "file1.txt": "",
4778 "file2.txt": ""
4779 },
4780 },
4781 "subdir2": {
4782 "file4.txt": ""
4783 }
4784 },
4785 "dir2": {
4786 "single_file": {
4787 "file5.txt": ""
4788 }
4789 }
4790 }),
4791 )
4792 .await;
4793
4794 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4795 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4796 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4797
4798 // Test 1: Basic collapsing
4799 {
4800 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4801
4802 toggle_expand_dir(&panel, "root/dir1", cx);
4803 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4804 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4805 toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
4806
4807 assert_eq!(
4808 visible_entries_as_strings(&panel, 0..20, cx),
4809 &[
4810 separator!("v root"),
4811 separator!(" v dir1"),
4812 separator!(" v subdir1"),
4813 separator!(" v nested1"),
4814 separator!(" file1.txt"),
4815 separator!(" file2.txt"),
4816 separator!(" v subdir2 <== selected"),
4817 separator!(" file4.txt"),
4818 separator!(" > dir2"),
4819 ],
4820 "Initial state with everything expanded"
4821 );
4822
4823 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4824 panel.update(cx, |panel, cx| {
4825 let project = panel.project.read(cx);
4826 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4827 panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4828 panel.update_visible_entries(None, cx);
4829 });
4830
4831 assert_eq!(
4832 visible_entries_as_strings(&panel, 0..20, cx),
4833 &["v root", " > dir1", " > dir2",],
4834 "All subdirs under dir1 should be collapsed"
4835 );
4836 }
4837
4838 // Test 2: With auto-fold enabled
4839 {
4840 cx.update(|_, cx| {
4841 let settings = *ProjectPanelSettings::get_global(cx);
4842 ProjectPanelSettings::override_global(
4843 ProjectPanelSettings {
4844 auto_fold_dirs: true,
4845 ..settings
4846 },
4847 cx,
4848 );
4849 });
4850
4851 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4852
4853 toggle_expand_dir(&panel, "root/dir1", cx);
4854 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4855 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4856
4857 assert_eq!(
4858 visible_entries_as_strings(&panel, 0..20, cx),
4859 &[
4860 separator!("v root"),
4861 separator!(" v dir1"),
4862 separator!(" v subdir1/nested1 <== selected"),
4863 separator!(" file1.txt"),
4864 separator!(" file2.txt"),
4865 separator!(" > subdir2"),
4866 separator!(" > dir2/single_file"),
4867 ],
4868 "Initial state with some dirs expanded"
4869 );
4870
4871 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4872 panel.update(cx, |panel, cx| {
4873 let project = panel.project.read(cx);
4874 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4875 panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4876 });
4877
4878 toggle_expand_dir(&panel, "root/dir1", cx);
4879
4880 assert_eq!(
4881 visible_entries_as_strings(&panel, 0..20, cx),
4882 &[
4883 separator!("v root"),
4884 separator!(" v dir1 <== selected"),
4885 separator!(" > subdir1/nested1"),
4886 separator!(" > subdir2"),
4887 separator!(" > dir2/single_file"),
4888 ],
4889 "Subdirs should be collapsed and folded with auto-fold enabled"
4890 );
4891 }
4892
4893 // Test 3: With auto-fold disabled
4894 {
4895 cx.update(|_, cx| {
4896 let settings = *ProjectPanelSettings::get_global(cx);
4897 ProjectPanelSettings::override_global(
4898 ProjectPanelSettings {
4899 auto_fold_dirs: false,
4900 ..settings
4901 },
4902 cx,
4903 );
4904 });
4905
4906 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4907
4908 toggle_expand_dir(&panel, "root/dir1", cx);
4909 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4910 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4911
4912 assert_eq!(
4913 visible_entries_as_strings(&panel, 0..20, cx),
4914 &[
4915 separator!("v root"),
4916 separator!(" v dir1"),
4917 separator!(" v subdir1"),
4918 separator!(" v nested1 <== selected"),
4919 separator!(" file1.txt"),
4920 separator!(" file2.txt"),
4921 separator!(" > subdir2"),
4922 separator!(" > dir2"),
4923 ],
4924 "Initial state with some dirs expanded and auto-fold disabled"
4925 );
4926
4927 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4928 panel.update(cx, |panel, cx| {
4929 let project = panel.project.read(cx);
4930 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4931 panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4932 });
4933
4934 toggle_expand_dir(&panel, "root/dir1", cx);
4935
4936 assert_eq!(
4937 visible_entries_as_strings(&panel, 0..20, cx),
4938 &[
4939 separator!("v root"),
4940 separator!(" v dir1 <== selected"),
4941 separator!(" > subdir1"),
4942 separator!(" > subdir2"),
4943 separator!(" > dir2"),
4944 ],
4945 "Subdirs should be collapsed but not folded with auto-fold disabled"
4946 );
4947 }
4948}
4949
4950fn select_path(panel: &Entity<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
4951 let path = path.as_ref();
4952 panel.update(cx, |panel, cx| {
4953 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4954 let worktree = worktree.read(cx);
4955 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4956 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
4957 panel.selection = Some(crate::SelectedEntry {
4958 worktree_id: worktree.id(),
4959 entry_id,
4960 });
4961 return;
4962 }
4963 }
4964 panic!("no worktree for path {:?}", path);
4965 });
4966}
4967
4968fn select_path_with_mark(
4969 panel: &Entity<ProjectPanel>,
4970 path: impl AsRef<Path>,
4971 cx: &mut VisualTestContext,
4972) {
4973 let path = path.as_ref();
4974 panel.update(cx, |panel, cx| {
4975 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4976 let worktree = worktree.read(cx);
4977 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4978 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
4979 let entry = crate::SelectedEntry {
4980 worktree_id: worktree.id(),
4981 entry_id,
4982 };
4983 if !panel.marked_entries.contains(&entry) {
4984 panel.marked_entries.insert(entry);
4985 }
4986 panel.selection = Some(entry);
4987 return;
4988 }
4989 }
4990 panic!("no worktree for path {:?}", path);
4991 });
4992}
4993
4994fn find_project_entry(
4995 panel: &Entity<ProjectPanel>,
4996 path: impl AsRef<Path>,
4997 cx: &mut VisualTestContext,
4998) -> Option<ProjectEntryId> {
4999 let path = path.as_ref();
5000 panel.update(cx, |panel, cx| {
5001 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
5002 let worktree = worktree.read(cx);
5003 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
5004 return worktree.entry_for_path(relative_path).map(|entry| entry.id);
5005 }
5006 }
5007 panic!("no worktree for path {path:?}");
5008 })
5009}
5010
5011fn visible_entries_as_strings(
5012 panel: &Entity<ProjectPanel>,
5013 range: Range<usize>,
5014 cx: &mut VisualTestContext,
5015) -> Vec<String> {
5016 let mut result = Vec::new();
5017 let mut project_entries = HashSet::default();
5018 let mut has_editor = false;
5019
5020 panel.update_in(cx, |panel, window, cx| {
5021 panel.for_each_visible_entry(range, window, cx, |project_entry, details, _, _| {
5022 if details.is_editing {
5023 assert!(!has_editor, "duplicate editor entry");
5024 has_editor = true;
5025 } else {
5026 assert!(
5027 project_entries.insert(project_entry),
5028 "duplicate project entry {:?} {:?}",
5029 project_entry,
5030 details
5031 );
5032 }
5033
5034 let indent = " ".repeat(details.depth);
5035 let icon = if details.kind.is_dir() {
5036 if details.is_expanded { "v " } else { "> " }
5037 } else {
5038 " "
5039 };
5040 let name = if details.is_editing {
5041 format!("[EDITOR: '{}']", details.filename)
5042 } else if details.is_processing {
5043 format!("[PROCESSING: '{}']", details.filename)
5044 } else {
5045 details.filename.clone()
5046 };
5047 let selected = if details.is_selected {
5048 " <== selected"
5049 } else {
5050 ""
5051 };
5052 let marked = if details.is_marked {
5053 " <== marked"
5054 } else {
5055 ""
5056 };
5057
5058 result.push(format!("{indent}{icon}{name}{selected}{marked}"));
5059 });
5060 });
5061
5062 result
5063}
5064
5065fn init_test(cx: &mut TestAppContext) {
5066 cx.update(|cx| {
5067 let settings_store = SettingsStore::test(cx);
5068 cx.set_global(settings_store);
5069 init_settings(cx);
5070 theme::init(theme::LoadThemes::JustBase, cx);
5071 language::init(cx);
5072 editor::init_settings(cx);
5073 crate::init(cx);
5074 workspace::init_settings(cx);
5075 client::init_settings(cx);
5076 Project::init_settings(cx);
5077
5078 cx.update_global::<SettingsStore, _>(|store, cx| {
5079 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5080 project_panel_settings.auto_fold_dirs = Some(false);
5081 });
5082 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5083 worktree_settings.file_scan_exclusions = Some(Vec::new());
5084 });
5085 });
5086 });
5087}
5088
5089fn init_test_with_editor(cx: &mut TestAppContext) {
5090 cx.update(|cx| {
5091 let app_state = AppState::test(cx);
5092 theme::init(theme::LoadThemes::JustBase, cx);
5093 init_settings(cx);
5094 language::init(cx);
5095 editor::init(cx);
5096 crate::init(cx);
5097 workspace::init(app_state.clone(), cx);
5098 Project::init_settings(cx);
5099
5100 cx.update_global::<SettingsStore, _>(|store, cx| {
5101 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5102 project_panel_settings.auto_fold_dirs = Some(false);
5103 });
5104 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5105 worktree_settings.file_scan_exclusions = Some(Vec::new());
5106 });
5107 });
5108 });
5109}
5110
5111fn ensure_single_file_is_opened(
5112 window: &WindowHandle<Workspace>,
5113 expected_path: &str,
5114 cx: &mut TestAppContext,
5115) {
5116 window
5117 .update(cx, |workspace, _, cx| {
5118 let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
5119 assert_eq!(worktrees.len(), 1);
5120 let worktree_id = worktrees[0].read(cx).id();
5121
5122 let open_project_paths = workspace
5123 .panes()
5124 .iter()
5125 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
5126 .collect::<Vec<_>>();
5127 assert_eq!(
5128 open_project_paths,
5129 vec![ProjectPath {
5130 worktree_id,
5131 path: Arc::from(Path::new(expected_path))
5132 }],
5133 "Should have opened file, selected in project panel"
5134 );
5135 })
5136 .unwrap();
5137}
5138
5139fn submit_deletion(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
5140 assert!(
5141 !cx.has_pending_prompt(),
5142 "Should have no prompts before the deletion"
5143 );
5144 panel.update_in(cx, |panel, window, cx| {
5145 panel.delete(&Delete { skip_prompt: false }, window, cx)
5146 });
5147 assert!(
5148 cx.has_pending_prompt(),
5149 "Should have a prompt after the deletion"
5150 );
5151 cx.simulate_prompt_answer("Delete");
5152 assert!(
5153 !cx.has_pending_prompt(),
5154 "Should have no prompts after prompt was replied to"
5155 );
5156 cx.executor().run_until_parked();
5157}
5158
5159fn submit_deletion_skipping_prompt(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
5160 assert!(
5161 !cx.has_pending_prompt(),
5162 "Should have no prompts before the deletion"
5163 );
5164 panel.update_in(cx, |panel, window, cx| {
5165 panel.delete(&Delete { skip_prompt: true }, window, cx)
5166 });
5167 assert!(!cx.has_pending_prompt(), "Should have received no prompts");
5168 cx.executor().run_until_parked();
5169}
5170
5171fn ensure_no_open_items_and_panes(workspace: &WindowHandle<Workspace>, cx: &mut VisualTestContext) {
5172 assert!(
5173 !cx.has_pending_prompt(),
5174 "Should have no prompts after deletion operation closes the file"
5175 );
5176 workspace
5177 .read_with(cx, |workspace, cx| {
5178 let open_project_paths = workspace
5179 .panes()
5180 .iter()
5181 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
5182 .collect::<Vec<_>>();
5183 assert!(
5184 open_project_paths.is_empty(),
5185 "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
5186 );
5187 })
5188 .unwrap();
5189}
5190
5191struct TestProjectItemView {
5192 focus_handle: FocusHandle,
5193 path: ProjectPath,
5194}
5195
5196struct TestProjectItem {
5197 path: ProjectPath,
5198}
5199
5200impl project::ProjectItem for TestProjectItem {
5201 fn try_open(
5202 _project: &Entity<Project>,
5203 path: &ProjectPath,
5204 cx: &mut App,
5205 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
5206 let path = path.clone();
5207 Some(cx.spawn(async move |cx| cx.new(|_| Self { path })))
5208 }
5209
5210 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
5211 None
5212 }
5213
5214 fn project_path(&self, _: &App) -> Option<ProjectPath> {
5215 Some(self.path.clone())
5216 }
5217
5218 fn is_dirty(&self) -> bool {
5219 false
5220 }
5221}
5222
5223impl ProjectItem for TestProjectItemView {
5224 type Item = TestProjectItem;
5225
5226 fn for_project_item(
5227 _: Entity<Project>,
5228 _: &Pane,
5229 project_item: Entity<Self::Item>,
5230 _: &mut Window,
5231 cx: &mut Context<Self>,
5232 ) -> Self
5233 where
5234 Self: Sized,
5235 {
5236 Self {
5237 path: project_item.update(cx, |project_item, _| project_item.path.clone()),
5238 focus_handle: cx.focus_handle(),
5239 }
5240 }
5241}
5242
5243impl Item for TestProjectItemView {
5244 type Event = ();
5245}
5246
5247impl EventEmitter<()> for TestProjectItemView {}
5248
5249impl Focusable for TestProjectItemView {
5250 fn focus_handle(&self, _: &App) -> FocusHandle {
5251 self.focus_handle.clone()
5252 }
5253}
5254
5255impl Render for TestProjectItemView {
5256 fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5257 Empty
5258 }
5259}