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 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2074 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2075 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2076
2077 // Check initial state
2078 assert_eq!(
2079 visible_entries_as_strings(&panel, 0..15, cx),
2080 &[
2081 "v tree1",
2082 " > .git",
2083 " > dir1",
2084 " > dir2",
2085 " modified4.txt",
2086 " unmodified3.txt",
2087 "v tree2",
2088 " > .git",
2089 " > dir3",
2090 " modified6.txt",
2091 " unmodified5.txt"
2092 ],
2093 );
2094
2095 // Test selecting next modified entry
2096 panel.update_in(cx, |panel, window, cx| {
2097 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2098 });
2099
2100 assert_eq!(
2101 visible_entries_as_strings(&panel, 0..6, cx),
2102 &[
2103 "v tree1",
2104 " > .git",
2105 " v dir1",
2106 " modified1.txt <== selected",
2107 " modified2.txt",
2108 " unmodified1.txt",
2109 ],
2110 );
2111
2112 panel.update_in(cx, |panel, window, cx| {
2113 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2114 });
2115
2116 assert_eq!(
2117 visible_entries_as_strings(&panel, 0..6, cx),
2118 &[
2119 "v tree1",
2120 " > .git",
2121 " v dir1",
2122 " modified1.txt",
2123 " modified2.txt <== selected",
2124 " unmodified1.txt",
2125 ],
2126 );
2127
2128 panel.update_in(cx, |panel, window, cx| {
2129 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2130 });
2131
2132 assert_eq!(
2133 visible_entries_as_strings(&panel, 6..9, cx),
2134 &[
2135 " v dir2",
2136 " modified3.txt <== selected",
2137 " unmodified2.txt",
2138 ],
2139 );
2140
2141 panel.update_in(cx, |panel, window, cx| {
2142 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2143 });
2144
2145 assert_eq!(
2146 visible_entries_as_strings(&panel, 9..11, cx),
2147 &[" modified4.txt <== selected", " unmodified3.txt",],
2148 );
2149
2150 panel.update_in(cx, |panel, window, cx| {
2151 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2152 });
2153
2154 assert_eq!(
2155 visible_entries_as_strings(&panel, 13..16, cx),
2156 &[
2157 " v dir3",
2158 " modified5.txt <== selected",
2159 " unmodified4.txt",
2160 ],
2161 );
2162
2163 panel.update_in(cx, |panel, window, cx| {
2164 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2165 });
2166
2167 assert_eq!(
2168 visible_entries_as_strings(&panel, 16..18, cx),
2169 &[" modified6.txt <== selected", " unmodified5.txt",],
2170 );
2171
2172 // Wraps around to first modified file
2173 panel.update_in(cx, |panel, window, cx| {
2174 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2175 });
2176
2177 assert_eq!(
2178 visible_entries_as_strings(&panel, 0..18, cx),
2179 &[
2180 "v tree1",
2181 " > .git",
2182 " v dir1",
2183 " modified1.txt <== selected",
2184 " modified2.txt",
2185 " unmodified1.txt",
2186 " v dir2",
2187 " modified3.txt",
2188 " unmodified2.txt",
2189 " modified4.txt",
2190 " unmodified3.txt",
2191 "v tree2",
2192 " > .git",
2193 " v dir3",
2194 " modified5.txt",
2195 " unmodified4.txt",
2196 " modified6.txt",
2197 " unmodified5.txt",
2198 ],
2199 );
2200
2201 // Wraps around again to last modified file
2202 panel.update_in(cx, |panel, window, cx| {
2203 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2204 });
2205
2206 assert_eq!(
2207 visible_entries_as_strings(&panel, 16..18, cx),
2208 &[" modified6.txt <== selected", " unmodified5.txt",],
2209 );
2210
2211 panel.update_in(cx, |panel, window, cx| {
2212 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2213 });
2214
2215 assert_eq!(
2216 visible_entries_as_strings(&panel, 13..16, cx),
2217 &[
2218 " v dir3",
2219 " modified5.txt <== selected",
2220 " unmodified4.txt",
2221 ],
2222 );
2223
2224 panel.update_in(cx, |panel, window, cx| {
2225 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2226 });
2227
2228 assert_eq!(
2229 visible_entries_as_strings(&panel, 9..11, cx),
2230 &[" modified4.txt <== selected", " unmodified3.txt",],
2231 );
2232
2233 panel.update_in(cx, |panel, window, cx| {
2234 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2235 });
2236
2237 assert_eq!(
2238 visible_entries_as_strings(&panel, 6..9, cx),
2239 &[
2240 " v dir2",
2241 " modified3.txt <== selected",
2242 " unmodified2.txt",
2243 ],
2244 );
2245
2246 panel.update_in(cx, |panel, window, cx| {
2247 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2248 });
2249
2250 assert_eq!(
2251 visible_entries_as_strings(&panel, 0..6, cx),
2252 &[
2253 "v tree1",
2254 " > .git",
2255 " v dir1",
2256 " modified1.txt",
2257 " modified2.txt <== selected",
2258 " unmodified1.txt",
2259 ],
2260 );
2261
2262 panel.update_in(cx, |panel, window, cx| {
2263 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2264 });
2265
2266 assert_eq!(
2267 visible_entries_as_strings(&panel, 0..6, cx),
2268 &[
2269 "v tree1",
2270 " > .git",
2271 " v dir1",
2272 " modified1.txt <== selected",
2273 " modified2.txt",
2274 " unmodified1.txt",
2275 ],
2276 );
2277}
2278
2279#[gpui::test]
2280async fn test_select_directory(cx: &mut gpui::TestAppContext) {
2281 init_test_with_editor(cx);
2282
2283 let fs = FakeFs::new(cx.executor().clone());
2284 fs.insert_tree(
2285 "/project_root",
2286 json!({
2287 "dir_1": {
2288 "nested_dir": {
2289 "file_a.py": "# File contents",
2290 }
2291 },
2292 "file_1.py": "# File contents",
2293 "dir_2": {
2294
2295 },
2296 "dir_3": {
2297
2298 },
2299 "file_2.py": "# File contents",
2300 "dir_4": {
2301
2302 },
2303 }),
2304 )
2305 .await;
2306
2307 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2308 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2309 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2310 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2311
2312 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2313 cx.executor().run_until_parked();
2314 select_path(&panel, "project_root/dir_1", cx);
2315 cx.executor().run_until_parked();
2316 assert_eq!(
2317 visible_entries_as_strings(&panel, 0..10, cx),
2318 &[
2319 "v project_root",
2320 " > dir_1 <== selected",
2321 " > dir_2",
2322 " > dir_3",
2323 " > dir_4",
2324 " file_1.py",
2325 " file_2.py",
2326 ]
2327 );
2328 panel.update_in(cx, |panel, window, cx| {
2329 panel.select_prev_directory(&SelectPrevDirectory, window, cx)
2330 });
2331
2332 assert_eq!(
2333 visible_entries_as_strings(&panel, 0..10, cx),
2334 &[
2335 "v project_root <== selected",
2336 " > dir_1",
2337 " > dir_2",
2338 " > dir_3",
2339 " > dir_4",
2340 " file_1.py",
2341 " file_2.py",
2342 ]
2343 );
2344
2345 panel.update_in(cx, |panel, window, cx| {
2346 panel.select_prev_directory(&SelectPrevDirectory, window, cx)
2347 });
2348
2349 assert_eq!(
2350 visible_entries_as_strings(&panel, 0..10, cx),
2351 &[
2352 "v project_root",
2353 " > dir_1",
2354 " > dir_2",
2355 " > dir_3",
2356 " > dir_4 <== selected",
2357 " file_1.py",
2358 " file_2.py",
2359 ]
2360 );
2361
2362 panel.update_in(cx, |panel, window, cx| {
2363 panel.select_next_directory(&SelectNextDirectory, window, cx)
2364 });
2365
2366 assert_eq!(
2367 visible_entries_as_strings(&panel, 0..10, cx),
2368 &[
2369 "v project_root <== selected",
2370 " > dir_1",
2371 " > dir_2",
2372 " > dir_3",
2373 " > dir_4",
2374 " file_1.py",
2375 " file_2.py",
2376 ]
2377 );
2378}
2379#[gpui::test]
2380async fn test_select_first_last(cx: &mut gpui::TestAppContext) {
2381 init_test_with_editor(cx);
2382
2383 let fs = FakeFs::new(cx.executor().clone());
2384 fs.insert_tree(
2385 "/project_root",
2386 json!({
2387 "dir_1": {
2388 "nested_dir": {
2389 "file_a.py": "# File contents",
2390 }
2391 },
2392 "file_1.py": "# File contents",
2393 "file_2.py": "# File contents",
2394 "zdir_2": {
2395 "nested_dir2": {
2396 "file_b.py": "# File contents",
2397 }
2398 },
2399 }),
2400 )
2401 .await;
2402
2403 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2404 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2405 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2406 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2407
2408 assert_eq!(
2409 visible_entries_as_strings(&panel, 0..10, cx),
2410 &[
2411 "v project_root",
2412 " > dir_1",
2413 " > zdir_2",
2414 " file_1.py",
2415 " file_2.py",
2416 ]
2417 );
2418 panel.update_in(cx, |panel, window, cx| {
2419 panel.select_first(&SelectFirst, window, cx)
2420 });
2421
2422 assert_eq!(
2423 visible_entries_as_strings(&panel, 0..10, cx),
2424 &[
2425 "v project_root <== selected",
2426 " > dir_1",
2427 " > zdir_2",
2428 " file_1.py",
2429 " file_2.py",
2430 ]
2431 );
2432
2433 panel.update_in(cx, |panel, window, cx| {
2434 panel.select_last(&SelectLast, window, cx)
2435 });
2436
2437 assert_eq!(
2438 visible_entries_as_strings(&panel, 0..10, cx),
2439 &[
2440 "v project_root",
2441 " > dir_1",
2442 " > zdir_2",
2443 " file_1.py",
2444 " file_2.py <== selected",
2445 ]
2446 );
2447}
2448
2449#[gpui::test]
2450async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) {
2451 init_test_with_editor(cx);
2452
2453 let fs = FakeFs::new(cx.executor().clone());
2454 fs.insert_tree(
2455 "/project_root",
2456 json!({
2457 "dir_1": {
2458 "nested_dir": {
2459 "file_a.py": "# File contents",
2460 }
2461 },
2462 "file_1.py": "# File contents",
2463 }),
2464 )
2465 .await;
2466
2467 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2468 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2469 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2470 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2471
2472 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2473 cx.executor().run_until_parked();
2474 select_path(&panel, "project_root/dir_1", cx);
2475 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2476 select_path(&panel, "project_root/dir_1/nested_dir", cx);
2477 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2478 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2479 cx.executor().run_until_parked();
2480 assert_eq!(
2481 visible_entries_as_strings(&panel, 0..10, cx),
2482 &[
2483 "v project_root",
2484 " v dir_1",
2485 " > nested_dir <== selected",
2486 " file_1.py",
2487 ]
2488 );
2489}
2490
2491#[gpui::test]
2492async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
2493 init_test_with_editor(cx);
2494
2495 let fs = FakeFs::new(cx.executor().clone());
2496 fs.insert_tree(
2497 "/project_root",
2498 json!({
2499 "dir_1": {
2500 "nested_dir": {
2501 "file_a.py": "# File contents",
2502 "file_b.py": "# File contents",
2503 "file_c.py": "# File contents",
2504 },
2505 "file_1.py": "# File contents",
2506 "file_2.py": "# File contents",
2507 "file_3.py": "# File contents",
2508 },
2509 "dir_2": {
2510 "file_1.py": "# File contents",
2511 "file_2.py": "# File contents",
2512 "file_3.py": "# File contents",
2513 }
2514 }),
2515 )
2516 .await;
2517
2518 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2519 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2520 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2521 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2522
2523 panel.update_in(cx, |panel, window, cx| {
2524 panel.collapse_all_entries(&CollapseAllEntries, window, cx)
2525 });
2526 cx.executor().run_until_parked();
2527 assert_eq!(
2528 visible_entries_as_strings(&panel, 0..10, cx),
2529 &["v project_root", " > dir_1", " > dir_2",]
2530 );
2531
2532 // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
2533 toggle_expand_dir(&panel, "project_root/dir_1", cx);
2534 cx.executor().run_until_parked();
2535 assert_eq!(
2536 visible_entries_as_strings(&panel, 0..10, cx),
2537 &[
2538 "v project_root",
2539 " v dir_1 <== selected",
2540 " > nested_dir",
2541 " file_1.py",
2542 " file_2.py",
2543 " file_3.py",
2544 " > dir_2",
2545 ]
2546 );
2547}
2548
2549#[gpui::test]
2550async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
2551 init_test(cx);
2552
2553 let fs = FakeFs::new(cx.executor().clone());
2554 fs.as_fake().insert_tree(path!("/root"), json!({})).await;
2555 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
2556 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2557 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2558 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2559
2560 // Make a new buffer with no backing file
2561 workspace
2562 .update(cx, |workspace, window, cx| {
2563 Editor::new_file(workspace, &Default::default(), window, cx)
2564 })
2565 .unwrap();
2566
2567 cx.executor().run_until_parked();
2568
2569 // "Save as" the buffer, creating a new backing file for it
2570 let save_task = workspace
2571 .update(cx, |workspace, window, cx| {
2572 workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
2573 })
2574 .unwrap();
2575
2576 cx.executor().run_until_parked();
2577 cx.simulate_new_path_selection(|_| Some(PathBuf::from(path!("/root/new"))));
2578 save_task.await.unwrap();
2579
2580 // Rename the file
2581 select_path(&panel, "root/new", cx);
2582 assert_eq!(
2583 visible_entries_as_strings(&panel, 0..10, cx),
2584 &["v root", " new <== selected <== marked"]
2585 );
2586 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2587 panel.update_in(cx, |panel, window, cx| {
2588 panel
2589 .filename_editor
2590 .update(cx, |editor, cx| editor.set_text("newer", window, cx));
2591 });
2592 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2593
2594 cx.executor().run_until_parked();
2595 assert_eq!(
2596 visible_entries_as_strings(&panel, 0..10, cx),
2597 &["v root", " newer <== selected"]
2598 );
2599
2600 workspace
2601 .update(cx, |workspace, window, cx| {
2602 workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
2603 })
2604 .unwrap()
2605 .await
2606 .unwrap();
2607
2608 cx.executor().run_until_parked();
2609 // assert that saving the file doesn't restore "new"
2610 assert_eq!(
2611 visible_entries_as_strings(&panel, 0..10, cx),
2612 &["v root", " newer <== selected"]
2613 );
2614}
2615
2616#[gpui::test]
2617#[cfg_attr(target_os = "windows", ignore)]
2618async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
2619 init_test_with_editor(cx);
2620
2621 let fs = FakeFs::new(cx.executor().clone());
2622 fs.insert_tree(
2623 "/root1",
2624 json!({
2625 "dir1": {
2626 "file1.txt": "content 1",
2627 },
2628 }),
2629 )
2630 .await;
2631
2632 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
2633 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2634 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2635 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2636
2637 toggle_expand_dir(&panel, "root1/dir1", cx);
2638
2639 assert_eq!(
2640 visible_entries_as_strings(&panel, 0..20, cx),
2641 &["v root1", " v dir1 <== selected", " file1.txt",],
2642 "Initial state with worktrees"
2643 );
2644
2645 select_path(&panel, "root1", cx);
2646 assert_eq!(
2647 visible_entries_as_strings(&panel, 0..20, cx),
2648 &["v root1 <== selected", " v dir1", " file1.txt",],
2649 );
2650
2651 // Rename root1 to new_root1
2652 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2653
2654 assert_eq!(
2655 visible_entries_as_strings(&panel, 0..20, cx),
2656 &[
2657 "v [EDITOR: 'root1'] <== selected",
2658 " v dir1",
2659 " file1.txt",
2660 ],
2661 );
2662
2663 let confirm = panel.update_in(cx, |panel, window, cx| {
2664 panel
2665 .filename_editor
2666 .update(cx, |editor, cx| editor.set_text("new_root1", window, cx));
2667 panel.confirm_edit(window, cx).unwrap()
2668 });
2669 confirm.await.unwrap();
2670 assert_eq!(
2671 visible_entries_as_strings(&panel, 0..20, cx),
2672 &[
2673 "v new_root1 <== selected",
2674 " v dir1",
2675 " file1.txt",
2676 ],
2677 "Should update worktree name"
2678 );
2679
2680 // Ensure internal paths have been updated
2681 select_path(&panel, "new_root1/dir1/file1.txt", cx);
2682 assert_eq!(
2683 visible_entries_as_strings(&panel, 0..20, cx),
2684 &[
2685 "v new_root1",
2686 " v dir1",
2687 " file1.txt <== selected",
2688 ],
2689 "Files in renamed worktree are selectable"
2690 );
2691}
2692
2693#[gpui::test]
2694async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
2695 init_test_with_editor(cx);
2696 let fs = FakeFs::new(cx.executor().clone());
2697 fs.insert_tree(
2698 "/project_root",
2699 json!({
2700 "dir_1": {
2701 "nested_dir": {
2702 "file_a.py": "# File contents",
2703 }
2704 },
2705 "file_1.py": "# File contents",
2706 }),
2707 )
2708 .await;
2709
2710 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2711 let worktree_id = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
2712 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2713 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2714 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2715 cx.update(|window, cx| {
2716 panel.update(cx, |this, cx| {
2717 this.select_next(&Default::default(), window, cx);
2718 this.expand_selected_entry(&Default::default(), window, cx);
2719 this.expand_selected_entry(&Default::default(), window, cx);
2720 this.select_next(&Default::default(), window, cx);
2721 this.expand_selected_entry(&Default::default(), window, cx);
2722 this.select_next(&Default::default(), window, cx);
2723 })
2724 });
2725 assert_eq!(
2726 visible_entries_as_strings(&panel, 0..10, cx),
2727 &[
2728 "v project_root",
2729 " v dir_1",
2730 " v nested_dir",
2731 " file_a.py <== selected",
2732 " file_1.py",
2733 ]
2734 );
2735 let modifiers_with_shift = gpui::Modifiers {
2736 shift: true,
2737 ..Default::default()
2738 };
2739 cx.run_until_parked();
2740 cx.simulate_modifiers_change(modifiers_with_shift);
2741 cx.update(|window, cx| {
2742 panel.update(cx, |this, cx| {
2743 this.select_next(&Default::default(), window, cx);
2744 })
2745 });
2746 assert_eq!(
2747 visible_entries_as_strings(&panel, 0..10, cx),
2748 &[
2749 "v project_root",
2750 " v dir_1",
2751 " v nested_dir",
2752 " file_a.py",
2753 " file_1.py <== selected <== marked",
2754 ]
2755 );
2756 cx.update(|window, cx| {
2757 panel.update(cx, |this, cx| {
2758 this.select_previous(&Default::default(), window, cx);
2759 })
2760 });
2761 assert_eq!(
2762 visible_entries_as_strings(&panel, 0..10, cx),
2763 &[
2764 "v project_root",
2765 " v dir_1",
2766 " v nested_dir",
2767 " file_a.py <== selected <== marked",
2768 " file_1.py <== marked",
2769 ]
2770 );
2771 cx.update(|window, cx| {
2772 panel.update(cx, |this, cx| {
2773 let drag = DraggedSelection {
2774 active_selection: this.selection.unwrap(),
2775 marked_selections: Arc::new(this.marked_entries.clone()),
2776 };
2777 let target_entry = this
2778 .project
2779 .read(cx)
2780 .entry_for_path(&(worktree_id, "").into(), cx)
2781 .unwrap();
2782 this.drag_onto(&drag, target_entry.id, false, window, cx);
2783 });
2784 });
2785 cx.run_until_parked();
2786 assert_eq!(
2787 visible_entries_as_strings(&panel, 0..10, cx),
2788 &[
2789 "v project_root",
2790 " v dir_1",
2791 " v nested_dir",
2792 " file_1.py <== marked",
2793 " file_a.py <== selected <== marked",
2794 ]
2795 );
2796 // ESC clears out all marks
2797 cx.update(|window, cx| {
2798 panel.update(cx, |this, cx| {
2799 this.cancel(&menu::Cancel, window, cx);
2800 })
2801 });
2802 assert_eq!(
2803 visible_entries_as_strings(&panel, 0..10, cx),
2804 &[
2805 "v project_root",
2806 " v dir_1",
2807 " v nested_dir",
2808 " file_1.py",
2809 " file_a.py <== selected",
2810 ]
2811 );
2812 // ESC clears out all marks
2813 cx.update(|window, cx| {
2814 panel.update(cx, |this, cx| {
2815 this.select_previous(&SelectPrevious, window, cx);
2816 this.select_next(&SelectNext, window, cx);
2817 })
2818 });
2819 assert_eq!(
2820 visible_entries_as_strings(&panel, 0..10, cx),
2821 &[
2822 "v project_root",
2823 " v dir_1",
2824 " v nested_dir",
2825 " file_1.py <== marked",
2826 " file_a.py <== selected <== marked",
2827 ]
2828 );
2829 cx.simulate_modifiers_change(Default::default());
2830 cx.update(|window, cx| {
2831 panel.update(cx, |this, cx| {
2832 this.cut(&Cut, window, cx);
2833 this.select_previous(&SelectPrevious, window, cx);
2834 this.select_previous(&SelectPrevious, window, cx);
2835
2836 this.paste(&Paste, window, cx);
2837 // this.expand_selected_entry(&ExpandSelectedEntry, cx);
2838 })
2839 });
2840 cx.run_until_parked();
2841 assert_eq!(
2842 visible_entries_as_strings(&panel, 0..10, cx),
2843 &[
2844 "v project_root",
2845 " v dir_1",
2846 " v nested_dir",
2847 " file_1.py <== marked",
2848 " file_a.py <== selected <== marked",
2849 ]
2850 );
2851 cx.simulate_modifiers_change(modifiers_with_shift);
2852 cx.update(|window, cx| {
2853 panel.update(cx, |this, cx| {
2854 this.expand_selected_entry(&Default::default(), window, cx);
2855 this.select_next(&SelectNext, window, cx);
2856 this.select_next(&SelectNext, window, cx);
2857 })
2858 });
2859 submit_deletion(&panel, cx);
2860 assert_eq!(
2861 visible_entries_as_strings(&panel, 0..10, cx),
2862 &[
2863 "v project_root",
2864 " v dir_1",
2865 " v nested_dir <== selected",
2866 ]
2867 );
2868}
2869#[gpui::test]
2870async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
2871 init_test_with_editor(cx);
2872 cx.update(|cx| {
2873 cx.update_global::<SettingsStore, _>(|store, cx| {
2874 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
2875 worktree_settings.file_scan_exclusions = Some(Vec::new());
2876 });
2877 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
2878 project_panel_settings.auto_reveal_entries = Some(false)
2879 });
2880 })
2881 });
2882
2883 let fs = FakeFs::new(cx.background_executor.clone());
2884 fs.insert_tree(
2885 "/project_root",
2886 json!({
2887 ".git": {},
2888 ".gitignore": "**/gitignored_dir",
2889 "dir_1": {
2890 "file_1.py": "# File 1_1 contents",
2891 "file_2.py": "# File 1_2 contents",
2892 "file_3.py": "# File 1_3 contents",
2893 "gitignored_dir": {
2894 "file_a.py": "# File contents",
2895 "file_b.py": "# File contents",
2896 "file_c.py": "# File contents",
2897 },
2898 },
2899 "dir_2": {
2900 "file_1.py": "# File 2_1 contents",
2901 "file_2.py": "# File 2_2 contents",
2902 "file_3.py": "# File 2_3 contents",
2903 }
2904 }),
2905 )
2906 .await;
2907
2908 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2909 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2910 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2911 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2912
2913 assert_eq!(
2914 visible_entries_as_strings(&panel, 0..20, cx),
2915 &[
2916 "v project_root",
2917 " > .git",
2918 " > dir_1",
2919 " > dir_2",
2920 " .gitignore",
2921 ]
2922 );
2923
2924 let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
2925 .expect("dir 1 file is not ignored and should have an entry");
2926 let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
2927 .expect("dir 2 file is not ignored and should have an entry");
2928 let gitignored_dir_file =
2929 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
2930 assert_eq!(
2931 gitignored_dir_file, None,
2932 "File in the gitignored dir should not have an entry before its dir is toggled"
2933 );
2934
2935 toggle_expand_dir(&panel, "project_root/dir_1", cx);
2936 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
2937 cx.executor().run_until_parked();
2938 assert_eq!(
2939 visible_entries_as_strings(&panel, 0..20, cx),
2940 &[
2941 "v project_root",
2942 " > .git",
2943 " v dir_1",
2944 " v gitignored_dir <== selected",
2945 " file_a.py",
2946 " file_b.py",
2947 " file_c.py",
2948 " file_1.py",
2949 " file_2.py",
2950 " file_3.py",
2951 " > dir_2",
2952 " .gitignore",
2953 ],
2954 "Should show gitignored dir file list in the project panel"
2955 );
2956 let gitignored_dir_file =
2957 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
2958 .expect("after gitignored dir got opened, a file entry should be present");
2959
2960 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
2961 toggle_expand_dir(&panel, "project_root/dir_1", cx);
2962 assert_eq!(
2963 visible_entries_as_strings(&panel, 0..20, cx),
2964 &[
2965 "v project_root",
2966 " > .git",
2967 " > dir_1 <== selected",
2968 " > dir_2",
2969 " .gitignore",
2970 ],
2971 "Should hide all dir contents again and prepare for the auto reveal test"
2972 );
2973
2974 for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
2975 panel.update(cx, |panel, cx| {
2976 panel.project.update(cx, |_, cx| {
2977 cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
2978 })
2979 });
2980 cx.run_until_parked();
2981 assert_eq!(
2982 visible_entries_as_strings(&panel, 0..20, cx),
2983 &[
2984 "v project_root",
2985 " > .git",
2986 " > dir_1 <== selected",
2987 " > dir_2",
2988 " .gitignore",
2989 ],
2990 "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
2991 );
2992 }
2993
2994 cx.update(|_, cx| {
2995 cx.update_global::<SettingsStore, _>(|store, cx| {
2996 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
2997 project_panel_settings.auto_reveal_entries = Some(true)
2998 });
2999 })
3000 });
3001
3002 panel.update(cx, |panel, cx| {
3003 panel.project.update(cx, |_, cx| {
3004 cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
3005 })
3006 });
3007 cx.run_until_parked();
3008 assert_eq!(
3009 visible_entries_as_strings(&panel, 0..20, cx),
3010 &[
3011 "v project_root",
3012 " > .git",
3013 " v dir_1",
3014 " > gitignored_dir",
3015 " file_1.py <== selected <== marked",
3016 " file_2.py",
3017 " file_3.py",
3018 " > dir_2",
3019 " .gitignore",
3020 ],
3021 "When auto reveal is enabled, not ignored dir_1 entry should be revealed"
3022 );
3023
3024 panel.update(cx, |panel, cx| {
3025 panel.project.update(cx, |_, cx| {
3026 cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
3027 })
3028 });
3029 cx.run_until_parked();
3030 assert_eq!(
3031 visible_entries_as_strings(&panel, 0..20, cx),
3032 &[
3033 "v project_root",
3034 " > .git",
3035 " v dir_1",
3036 " > gitignored_dir",
3037 " file_1.py",
3038 " file_2.py",
3039 " file_3.py",
3040 " v dir_2",
3041 " file_1.py <== selected <== marked",
3042 " file_2.py",
3043 " file_3.py",
3044 " .gitignore",
3045 ],
3046 "When auto reveal is enabled, not ignored dir_2 entry should be revealed"
3047 );
3048
3049 panel.update(cx, |panel, cx| {
3050 panel.project.update(cx, |_, cx| {
3051 cx.emit(project::Event::ActiveEntryChanged(Some(
3052 gitignored_dir_file,
3053 )))
3054 })
3055 });
3056 cx.run_until_parked();
3057 assert_eq!(
3058 visible_entries_as_strings(&panel, 0..20, cx),
3059 &[
3060 "v project_root",
3061 " > .git",
3062 " v dir_1",
3063 " > gitignored_dir",
3064 " file_1.py",
3065 " file_2.py",
3066 " file_3.py",
3067 " v dir_2",
3068 " file_1.py <== selected <== marked",
3069 " file_2.py",
3070 " file_3.py",
3071 " .gitignore",
3072 ],
3073 "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
3074 );
3075
3076 panel.update(cx, |panel, cx| {
3077 panel.project.update(cx, |_, cx| {
3078 cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
3079 })
3080 });
3081 cx.run_until_parked();
3082 assert_eq!(
3083 visible_entries_as_strings(&panel, 0..20, cx),
3084 &[
3085 "v project_root",
3086 " > .git",
3087 " v dir_1",
3088 " v gitignored_dir",
3089 " file_a.py <== selected <== marked",
3090 " file_b.py",
3091 " file_c.py",
3092 " file_1.py",
3093 " file_2.py",
3094 " file_3.py",
3095 " v dir_2",
3096 " file_1.py",
3097 " file_2.py",
3098 " file_3.py",
3099 " .gitignore",
3100 ],
3101 "When a gitignored entry is explicitly revealed, it should be shown in the project tree"
3102 );
3103}
3104
3105#[gpui::test]
3106async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
3107 init_test_with_editor(cx);
3108 cx.update(|cx| {
3109 cx.update_global::<SettingsStore, _>(|store, cx| {
3110 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3111 worktree_settings.file_scan_exclusions = Some(Vec::new());
3112 worktree_settings.file_scan_inclusions =
3113 Some(vec!["always_included_but_ignored_dir/*".to_string()]);
3114 });
3115 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3116 project_panel_settings.auto_reveal_entries = Some(false)
3117 });
3118 })
3119 });
3120
3121 let fs = FakeFs::new(cx.background_executor.clone());
3122 fs.insert_tree(
3123 "/project_root",
3124 json!({
3125 ".git": {},
3126 ".gitignore": "**/gitignored_dir\n/always_included_but_ignored_dir",
3127 "dir_1": {
3128 "file_1.py": "# File 1_1 contents",
3129 "file_2.py": "# File 1_2 contents",
3130 "file_3.py": "# File 1_3 contents",
3131 "gitignored_dir": {
3132 "file_a.py": "# File contents",
3133 "file_b.py": "# File contents",
3134 "file_c.py": "# File contents",
3135 },
3136 },
3137 "dir_2": {
3138 "file_1.py": "# File 2_1 contents",
3139 "file_2.py": "# File 2_2 contents",
3140 "file_3.py": "# File 2_3 contents",
3141 },
3142 "always_included_but_ignored_dir": {
3143 "file_a.py": "# File contents",
3144 "file_b.py": "# File contents",
3145 "file_c.py": "# File contents",
3146 },
3147 }),
3148 )
3149 .await;
3150
3151 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3152 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3153 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3154 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3155
3156 assert_eq!(
3157 visible_entries_as_strings(&panel, 0..20, cx),
3158 &[
3159 "v project_root",
3160 " > .git",
3161 " > always_included_but_ignored_dir",
3162 " > dir_1",
3163 " > dir_2",
3164 " .gitignore",
3165 ]
3166 );
3167
3168 let gitignored_dir_file =
3169 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3170 let always_included_but_ignored_dir_file = find_project_entry(
3171 &panel,
3172 "project_root/always_included_but_ignored_dir/file_a.py",
3173 cx,
3174 )
3175 .expect("file that is .gitignored but set to always be included should have an entry");
3176 assert_eq!(
3177 gitignored_dir_file, None,
3178 "File in the gitignored dir should not have an entry unless its directory is toggled"
3179 );
3180
3181 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3182 cx.run_until_parked();
3183 cx.update(|_, cx| {
3184 cx.update_global::<SettingsStore, _>(|store, cx| {
3185 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3186 project_panel_settings.auto_reveal_entries = Some(true)
3187 });
3188 })
3189 });
3190
3191 panel.update(cx, |panel, cx| {
3192 panel.project.update(cx, |_, cx| {
3193 cx.emit(project::Event::ActiveEntryChanged(Some(
3194 always_included_but_ignored_dir_file,
3195 )))
3196 })
3197 });
3198 cx.run_until_parked();
3199
3200 assert_eq!(
3201 visible_entries_as_strings(&panel, 0..20, cx),
3202 &[
3203 "v project_root",
3204 " > .git",
3205 " v always_included_but_ignored_dir",
3206 " file_a.py <== selected <== marked",
3207 " file_b.py",
3208 " file_c.py",
3209 " v dir_1",
3210 " > gitignored_dir",
3211 " file_1.py",
3212 " file_2.py",
3213 " file_3.py",
3214 " > dir_2",
3215 " .gitignore",
3216 ],
3217 "When auto reveal is enabled, a gitignored but always included selected entry should be revealed in the project panel"
3218 );
3219}
3220
3221#[gpui::test]
3222async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
3223 init_test_with_editor(cx);
3224 cx.update(|cx| {
3225 cx.update_global::<SettingsStore, _>(|store, cx| {
3226 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3227 worktree_settings.file_scan_exclusions = Some(Vec::new());
3228 });
3229 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3230 project_panel_settings.auto_reveal_entries = Some(false)
3231 });
3232 })
3233 });
3234
3235 let fs = FakeFs::new(cx.background_executor.clone());
3236 fs.insert_tree(
3237 "/project_root",
3238 json!({
3239 ".git": {},
3240 ".gitignore": "**/gitignored_dir",
3241 "dir_1": {
3242 "file_1.py": "# File 1_1 contents",
3243 "file_2.py": "# File 1_2 contents",
3244 "file_3.py": "# File 1_3 contents",
3245 "gitignored_dir": {
3246 "file_a.py": "# File contents",
3247 "file_b.py": "# File contents",
3248 "file_c.py": "# File contents",
3249 },
3250 },
3251 "dir_2": {
3252 "file_1.py": "# File 2_1 contents",
3253 "file_2.py": "# File 2_2 contents",
3254 "file_3.py": "# File 2_3 contents",
3255 }
3256 }),
3257 )
3258 .await;
3259
3260 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3261 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3262 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3263 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3264
3265 assert_eq!(
3266 visible_entries_as_strings(&panel, 0..20, cx),
3267 &[
3268 "v project_root",
3269 " > .git",
3270 " > dir_1",
3271 " > dir_2",
3272 " .gitignore",
3273 ]
3274 );
3275
3276 let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
3277 .expect("dir 1 file is not ignored and should have an entry");
3278 let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
3279 .expect("dir 2 file is not ignored and should have an entry");
3280 let gitignored_dir_file =
3281 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3282 assert_eq!(
3283 gitignored_dir_file, None,
3284 "File in the gitignored dir should not have an entry before its dir is toggled"
3285 );
3286
3287 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3288 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3289 cx.run_until_parked();
3290 assert_eq!(
3291 visible_entries_as_strings(&panel, 0..20, cx),
3292 &[
3293 "v project_root",
3294 " > .git",
3295 " v dir_1",
3296 " v gitignored_dir <== selected",
3297 " file_a.py",
3298 " file_b.py",
3299 " file_c.py",
3300 " file_1.py",
3301 " file_2.py",
3302 " file_3.py",
3303 " > dir_2",
3304 " .gitignore",
3305 ],
3306 "Should show gitignored dir file list in the project panel"
3307 );
3308 let gitignored_dir_file =
3309 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
3310 .expect("after gitignored dir got opened, a file entry should be present");
3311
3312 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3313 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3314 assert_eq!(
3315 visible_entries_as_strings(&panel, 0..20, cx),
3316 &[
3317 "v project_root",
3318 " > .git",
3319 " > dir_1 <== selected",
3320 " > dir_2",
3321 " .gitignore",
3322 ],
3323 "Should hide all dir contents again and prepare for the explicit reveal test"
3324 );
3325
3326 for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
3327 panel.update(cx, |panel, cx| {
3328 panel.project.update(cx, |_, cx| {
3329 cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
3330 })
3331 });
3332 cx.run_until_parked();
3333 assert_eq!(
3334 visible_entries_as_strings(&panel, 0..20, cx),
3335 &[
3336 "v project_root",
3337 " > .git",
3338 " > dir_1 <== selected",
3339 " > dir_2",
3340 " .gitignore",
3341 ],
3342 "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
3343 );
3344 }
3345
3346 panel.update(cx, |panel, cx| {
3347 panel.project.update(cx, |_, cx| {
3348 cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
3349 })
3350 });
3351 cx.run_until_parked();
3352 assert_eq!(
3353 visible_entries_as_strings(&panel, 0..20, cx),
3354 &[
3355 "v project_root",
3356 " > .git",
3357 " v dir_1",
3358 " > gitignored_dir",
3359 " file_1.py <== selected <== marked",
3360 " file_2.py",
3361 " file_3.py",
3362 " > dir_2",
3363 " .gitignore",
3364 ],
3365 "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
3366 );
3367
3368 panel.update(cx, |panel, cx| {
3369 panel.project.update(cx, |_, cx| {
3370 cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
3371 })
3372 });
3373 cx.run_until_parked();
3374 assert_eq!(
3375 visible_entries_as_strings(&panel, 0..20, cx),
3376 &[
3377 "v project_root",
3378 " > .git",
3379 " v dir_1",
3380 " > gitignored_dir",
3381 " file_1.py",
3382 " file_2.py",
3383 " file_3.py",
3384 " v dir_2",
3385 " file_1.py <== selected <== marked",
3386 " file_2.py",
3387 " file_3.py",
3388 " .gitignore",
3389 ],
3390 "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
3391 );
3392
3393 panel.update(cx, |panel, cx| {
3394 panel.project.update(cx, |_, cx| {
3395 cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
3396 })
3397 });
3398 cx.run_until_parked();
3399 assert_eq!(
3400 visible_entries_as_strings(&panel, 0..20, cx),
3401 &[
3402 "v project_root",
3403 " > .git",
3404 " v dir_1",
3405 " v gitignored_dir",
3406 " file_a.py <== selected <== marked",
3407 " file_b.py",
3408 " file_c.py",
3409 " file_1.py",
3410 " file_2.py",
3411 " file_3.py",
3412 " v dir_2",
3413 " file_1.py",
3414 " file_2.py",
3415 " file_3.py",
3416 " .gitignore",
3417 ],
3418 "With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
3419 );
3420}
3421
3422#[gpui::test]
3423async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
3424 init_test(cx);
3425 cx.update(|cx| {
3426 cx.update_global::<SettingsStore, _>(|store, cx| {
3427 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
3428 project_settings.file_scan_exclusions =
3429 Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
3430 });
3431 });
3432 });
3433
3434 cx.update(|cx| {
3435 register_project_item::<TestProjectItemView>(cx);
3436 });
3437
3438 let fs = FakeFs::new(cx.executor().clone());
3439 fs.insert_tree(
3440 "/root1",
3441 json!({
3442 ".dockerignore": "",
3443 ".git": {
3444 "HEAD": "",
3445 },
3446 }),
3447 )
3448 .await;
3449
3450 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
3451 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3452 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3453 let panel = workspace
3454 .update(cx, |workspace, window, cx| {
3455 let panel = ProjectPanel::new(workspace, window, cx);
3456 workspace.add_panel(panel.clone(), window, cx);
3457 panel
3458 })
3459 .unwrap();
3460
3461 select_path(&panel, "root1", cx);
3462 assert_eq!(
3463 visible_entries_as_strings(&panel, 0..10, cx),
3464 &["v root1 <== selected", " .dockerignore",]
3465 );
3466 workspace
3467 .update(cx, |workspace, _, cx| {
3468 assert!(
3469 workspace.active_item(cx).is_none(),
3470 "Should have no active items in the beginning"
3471 );
3472 })
3473 .unwrap();
3474
3475 let excluded_file_path = ".git/COMMIT_EDITMSG";
3476 let excluded_dir_path = "excluded_dir";
3477
3478 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
3479 panel.update_in(cx, |panel, window, cx| {
3480 assert!(panel.filename_editor.read(cx).is_focused(window));
3481 });
3482 panel
3483 .update_in(cx, |panel, window, cx| {
3484 panel.filename_editor.update(cx, |editor, cx| {
3485 editor.set_text(excluded_file_path, window, cx)
3486 });
3487 panel.confirm_edit(window, cx).unwrap()
3488 })
3489 .await
3490 .unwrap();
3491
3492 assert_eq!(
3493 visible_entries_as_strings(&panel, 0..13, cx),
3494 &["v root1", " .dockerignore"],
3495 "Excluded dir should not be shown after opening a file in it"
3496 );
3497 panel.update_in(cx, |panel, window, cx| {
3498 assert!(
3499 !panel.filename_editor.read(cx).is_focused(window),
3500 "Should have closed the file name editor"
3501 );
3502 });
3503 workspace
3504 .update(cx, |workspace, _, cx| {
3505 let active_entry_path = workspace
3506 .active_item(cx)
3507 .expect("should have opened and activated the excluded item")
3508 .act_as::<TestProjectItemView>(cx)
3509 .expect("should have opened the corresponding project item for the excluded item")
3510 .read(cx)
3511 .path
3512 .clone();
3513 assert_eq!(
3514 active_entry_path.path.as_ref(),
3515 Path::new(excluded_file_path),
3516 "Should open the excluded file"
3517 );
3518
3519 assert!(
3520 workspace.notification_ids().is_empty(),
3521 "Should have no notifications after opening an excluded file"
3522 );
3523 })
3524 .unwrap();
3525 assert!(
3526 fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
3527 "Should have created the excluded file"
3528 );
3529
3530 select_path(&panel, "root1", cx);
3531 panel.update_in(cx, |panel, window, cx| {
3532 panel.new_directory(&NewDirectory, window, cx)
3533 });
3534 panel.update_in(cx, |panel, window, cx| {
3535 assert!(panel.filename_editor.read(cx).is_focused(window));
3536 });
3537 panel
3538 .update_in(cx, |panel, window, cx| {
3539 panel.filename_editor.update(cx, |editor, cx| {
3540 editor.set_text(excluded_file_path, window, cx)
3541 });
3542 panel.confirm_edit(window, cx).unwrap()
3543 })
3544 .await
3545 .unwrap();
3546
3547 assert_eq!(
3548 visible_entries_as_strings(&panel, 0..13, cx),
3549 &["v root1", " .dockerignore"],
3550 "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
3551 );
3552 panel.update_in(cx, |panel, window, cx| {
3553 assert!(
3554 !panel.filename_editor.read(cx).is_focused(window),
3555 "Should have closed the file name editor"
3556 );
3557 });
3558 workspace
3559 .update(cx, |workspace, _, cx| {
3560 let notifications = workspace.notification_ids();
3561 assert_eq!(
3562 notifications.len(),
3563 1,
3564 "Should receive one notification with the error message"
3565 );
3566 workspace.dismiss_notification(notifications.first().unwrap(), cx);
3567 assert!(workspace.notification_ids().is_empty());
3568 })
3569 .unwrap();
3570
3571 select_path(&panel, "root1", cx);
3572 panel.update_in(cx, |panel, window, cx| {
3573 panel.new_directory(&NewDirectory, window, cx)
3574 });
3575 panel.update_in(cx, |panel, window, cx| {
3576 assert!(panel.filename_editor.read(cx).is_focused(window));
3577 });
3578 panel
3579 .update_in(cx, |panel, window, cx| {
3580 panel.filename_editor.update(cx, |editor, cx| {
3581 editor.set_text(excluded_dir_path, window, cx)
3582 });
3583 panel.confirm_edit(window, cx).unwrap()
3584 })
3585 .await
3586 .unwrap();
3587
3588 assert_eq!(
3589 visible_entries_as_strings(&panel, 0..13, cx),
3590 &["v root1", " .dockerignore"],
3591 "Should not change the project panel after trying to create an excluded directory"
3592 );
3593 panel.update_in(cx, |panel, window, cx| {
3594 assert!(
3595 !panel.filename_editor.read(cx).is_focused(window),
3596 "Should have closed the file name editor"
3597 );
3598 });
3599 workspace
3600 .update(cx, |workspace, _, cx| {
3601 let notifications = workspace.notification_ids();
3602 assert_eq!(
3603 notifications.len(),
3604 1,
3605 "Should receive one notification explaining that no directory is actually shown"
3606 );
3607 workspace.dismiss_notification(notifications.first().unwrap(), cx);
3608 assert!(workspace.notification_ids().is_empty());
3609 })
3610 .unwrap();
3611 assert!(
3612 fs.is_dir(Path::new("/root1/excluded_dir")).await,
3613 "Should have created the excluded directory"
3614 );
3615}
3616
3617#[gpui::test]
3618async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) {
3619 init_test_with_editor(cx);
3620
3621 let fs = FakeFs::new(cx.executor().clone());
3622 fs.insert_tree(
3623 "/src",
3624 json!({
3625 "test": {
3626 "first.rs": "// First Rust file",
3627 "second.rs": "// Second Rust file",
3628 "third.rs": "// Third Rust file",
3629 }
3630 }),
3631 )
3632 .await;
3633
3634 let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
3635 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3636 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3637 let panel = workspace
3638 .update(cx, |workspace, window, cx| {
3639 let panel = ProjectPanel::new(workspace, window, cx);
3640 workspace.add_panel(panel.clone(), window, cx);
3641 panel
3642 })
3643 .unwrap();
3644
3645 select_path(&panel, "src/", cx);
3646 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
3647 cx.executor().run_until_parked();
3648 assert_eq!(
3649 visible_entries_as_strings(&panel, 0..10, cx),
3650 &[
3651 //
3652 "v src <== selected",
3653 " > test"
3654 ]
3655 );
3656 panel.update_in(cx, |panel, window, cx| {
3657 panel.new_directory(&NewDirectory, window, cx)
3658 });
3659 panel.update_in(cx, |panel, window, cx| {
3660 assert!(panel.filename_editor.read(cx).is_focused(window));
3661 });
3662 assert_eq!(
3663 visible_entries_as_strings(&panel, 0..10, cx),
3664 &[
3665 //
3666 "v src",
3667 " > [EDITOR: ''] <== selected",
3668 " > test"
3669 ]
3670 );
3671
3672 panel.update_in(cx, |panel, window, cx| {
3673 panel.cancel(&menu::Cancel, window, cx)
3674 });
3675 assert_eq!(
3676 visible_entries_as_strings(&panel, 0..10, cx),
3677 &[
3678 //
3679 "v src <== selected",
3680 " > test"
3681 ]
3682 );
3683}
3684
3685#[gpui::test]
3686async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
3687 init_test_with_editor(cx);
3688
3689 let fs = FakeFs::new(cx.executor().clone());
3690 fs.insert_tree(
3691 "/root",
3692 json!({
3693 "dir1": {
3694 "subdir1": {},
3695 "file1.txt": "",
3696 "file2.txt": "",
3697 },
3698 "dir2": {
3699 "subdir2": {},
3700 "file3.txt": "",
3701 "file4.txt": "",
3702 },
3703 "file5.txt": "",
3704 "file6.txt": "",
3705 }),
3706 )
3707 .await;
3708
3709 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
3710 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3711 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3712 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3713
3714 toggle_expand_dir(&panel, "root/dir1", cx);
3715 toggle_expand_dir(&panel, "root/dir2", cx);
3716
3717 // Test Case 1: Delete middle file in directory
3718 select_path(&panel, "root/dir1/file1.txt", cx);
3719 assert_eq!(
3720 visible_entries_as_strings(&panel, 0..15, cx),
3721 &[
3722 "v root",
3723 " v dir1",
3724 " > subdir1",
3725 " file1.txt <== selected",
3726 " file2.txt",
3727 " v dir2",
3728 " > subdir2",
3729 " file3.txt",
3730 " file4.txt",
3731 " file5.txt",
3732 " file6.txt",
3733 ],
3734 "Initial state before deleting middle file"
3735 );
3736
3737 submit_deletion(&panel, cx);
3738 assert_eq!(
3739 visible_entries_as_strings(&panel, 0..15, cx),
3740 &[
3741 "v root",
3742 " v dir1",
3743 " > subdir1",
3744 " file2.txt <== selected",
3745 " v dir2",
3746 " > subdir2",
3747 " file3.txt",
3748 " file4.txt",
3749 " file5.txt",
3750 " file6.txt",
3751 ],
3752 "Should select next file after deleting middle file"
3753 );
3754
3755 // Test Case 2: Delete last file in directory
3756 submit_deletion(&panel, cx);
3757 assert_eq!(
3758 visible_entries_as_strings(&panel, 0..15, cx),
3759 &[
3760 "v root",
3761 " v dir1",
3762 " > subdir1 <== selected",
3763 " v dir2",
3764 " > subdir2",
3765 " file3.txt",
3766 " file4.txt",
3767 " file5.txt",
3768 " file6.txt",
3769 ],
3770 "Should select next directory when last file is deleted"
3771 );
3772
3773 // Test Case 3: Delete root level file
3774 select_path(&panel, "root/file6.txt", cx);
3775 assert_eq!(
3776 visible_entries_as_strings(&panel, 0..15, cx),
3777 &[
3778 "v root",
3779 " v dir1",
3780 " > subdir1",
3781 " v dir2",
3782 " > subdir2",
3783 " file3.txt",
3784 " file4.txt",
3785 " file5.txt",
3786 " file6.txt <== selected",
3787 ],
3788 "Initial state before deleting root level file"
3789 );
3790
3791 submit_deletion(&panel, cx);
3792 assert_eq!(
3793 visible_entries_as_strings(&panel, 0..15, cx),
3794 &[
3795 "v root",
3796 " v dir1",
3797 " > subdir1",
3798 " v dir2",
3799 " > subdir2",
3800 " file3.txt",
3801 " file4.txt",
3802 " file5.txt <== selected",
3803 ],
3804 "Should select prev entry at root level"
3805 );
3806}
3807
3808#[gpui::test]
3809async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) {
3810 init_test_with_editor(cx);
3811
3812 let fs = FakeFs::new(cx.executor().clone());
3813 fs.insert_tree(
3814 path!("/root"),
3815 json!({
3816 "aa": "// Testing 1",
3817 "bb": "// Testing 2",
3818 "cc": "// Testing 3",
3819 "dd": "// Testing 4",
3820 "ee": "// Testing 5",
3821 "ff": "// Testing 6",
3822 "gg": "// Testing 7",
3823 "hh": "// Testing 8",
3824 "ii": "// Testing 8",
3825 ".gitignore": "bb\ndd\nee\nff\nii\n'",
3826 }),
3827 )
3828 .await;
3829
3830 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3831 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3832 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3833
3834 // Test 1: Auto selection with one gitignored file next to the deleted file
3835 cx.update(|_, cx| {
3836 let settings = *ProjectPanelSettings::get_global(cx);
3837 ProjectPanelSettings::override_global(
3838 ProjectPanelSettings {
3839 hide_gitignore: true,
3840 ..settings
3841 },
3842 cx,
3843 );
3844 });
3845
3846 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3847
3848 select_path(&panel, "root/aa", cx);
3849 assert_eq!(
3850 visible_entries_as_strings(&panel, 0..10, cx),
3851 &[
3852 "v root",
3853 " .gitignore",
3854 " aa <== selected",
3855 " cc",
3856 " gg",
3857 " hh"
3858 ],
3859 "Initial state should hide files on .gitignore"
3860 );
3861
3862 submit_deletion(&panel, cx);
3863
3864 assert_eq!(
3865 visible_entries_as_strings(&panel, 0..10, cx),
3866 &[
3867 "v root",
3868 " .gitignore",
3869 " cc <== selected",
3870 " gg",
3871 " hh"
3872 ],
3873 "Should select next entry not on .gitignore"
3874 );
3875
3876 // Test 2: Auto selection with many gitignored files next to the deleted file
3877 submit_deletion(&panel, cx);
3878 assert_eq!(
3879 visible_entries_as_strings(&panel, 0..10, cx),
3880 &[
3881 "v root",
3882 " .gitignore",
3883 " gg <== selected",
3884 " hh"
3885 ],
3886 "Should select next entry not on .gitignore"
3887 );
3888
3889 // Test 3: Auto selection of entry before deleted file
3890 select_path(&panel, "root/hh", cx);
3891 assert_eq!(
3892 visible_entries_as_strings(&panel, 0..10, cx),
3893 &[
3894 "v root",
3895 " .gitignore",
3896 " gg",
3897 " hh <== selected"
3898 ],
3899 "Should select next entry not on .gitignore"
3900 );
3901 submit_deletion(&panel, cx);
3902 assert_eq!(
3903 visible_entries_as_strings(&panel, 0..10, cx),
3904 &["v root", " .gitignore", " gg <== selected"],
3905 "Should select next entry not on .gitignore"
3906 );
3907}
3908
3909#[gpui::test]
3910async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) {
3911 init_test_with_editor(cx);
3912
3913 let fs = FakeFs::new(cx.executor().clone());
3914 fs.insert_tree(
3915 path!("/root"),
3916 json!({
3917 "dir1": {
3918 "file1": "// Testing",
3919 "file2": "// Testing",
3920 "file3": "// Testing"
3921 },
3922 "aa": "// Testing",
3923 ".gitignore": "file1\nfile3\n",
3924 }),
3925 )
3926 .await;
3927
3928 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3929 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3930 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3931
3932 cx.update(|_, cx| {
3933 let settings = *ProjectPanelSettings::get_global(cx);
3934 ProjectPanelSettings::override_global(
3935 ProjectPanelSettings {
3936 hide_gitignore: true,
3937 ..settings
3938 },
3939 cx,
3940 );
3941 });
3942
3943 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3944
3945 // Test 1: Visible items should exclude files on gitignore
3946 toggle_expand_dir(&panel, "root/dir1", cx);
3947 select_path(&panel, "root/dir1/file2", cx);
3948 assert_eq!(
3949 visible_entries_as_strings(&panel, 0..10, cx),
3950 &[
3951 "v root",
3952 " v dir1",
3953 " file2 <== selected",
3954 " .gitignore",
3955 " aa"
3956 ],
3957 "Initial state should hide files on .gitignore"
3958 );
3959 submit_deletion(&panel, cx);
3960
3961 // Test 2: Auto selection should go to the parent
3962 assert_eq!(
3963 visible_entries_as_strings(&panel, 0..10, cx),
3964 &[
3965 "v root",
3966 " v dir1 <== selected",
3967 " .gitignore",
3968 " aa"
3969 ],
3970 "Initial state should hide files on .gitignore"
3971 );
3972}
3973
3974#[gpui::test]
3975async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
3976 init_test_with_editor(cx);
3977
3978 let fs = FakeFs::new(cx.executor().clone());
3979 fs.insert_tree(
3980 "/root",
3981 json!({
3982 "dir1": {
3983 "subdir1": {
3984 "a.txt": "",
3985 "b.txt": ""
3986 },
3987 "file1.txt": "",
3988 },
3989 "dir2": {
3990 "subdir2": {
3991 "c.txt": "",
3992 "d.txt": ""
3993 },
3994 "file2.txt": "",
3995 },
3996 "file3.txt": "",
3997 }),
3998 )
3999 .await;
4000
4001 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4002 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4003 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4004 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4005
4006 toggle_expand_dir(&panel, "root/dir1", cx);
4007 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4008 toggle_expand_dir(&panel, "root/dir2", cx);
4009 toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
4010
4011 // Test Case 1: Select and delete nested directory with parent
4012 cx.simulate_modifiers_change(gpui::Modifiers {
4013 control: true,
4014 ..Default::default()
4015 });
4016 select_path_with_mark(&panel, "root/dir1/subdir1", cx);
4017 select_path_with_mark(&panel, "root/dir1", cx);
4018
4019 assert_eq!(
4020 visible_entries_as_strings(&panel, 0..15, cx),
4021 &[
4022 "v root",
4023 " v dir1 <== selected <== marked",
4024 " v subdir1 <== marked",
4025 " a.txt",
4026 " b.txt",
4027 " file1.txt",
4028 " v dir2",
4029 " v subdir2",
4030 " c.txt",
4031 " d.txt",
4032 " file2.txt",
4033 " file3.txt",
4034 ],
4035 "Initial state before deleting nested directory with parent"
4036 );
4037
4038 submit_deletion(&panel, cx);
4039 assert_eq!(
4040 visible_entries_as_strings(&panel, 0..15, cx),
4041 &[
4042 "v root",
4043 " v dir2 <== selected",
4044 " v subdir2",
4045 " c.txt",
4046 " d.txt",
4047 " file2.txt",
4048 " file3.txt",
4049 ],
4050 "Should select next directory after deleting directory with parent"
4051 );
4052
4053 // Test Case 2: Select mixed files and directories across levels
4054 select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx);
4055 select_path_with_mark(&panel, "root/dir2/file2.txt", cx);
4056 select_path_with_mark(&panel, "root/file3.txt", cx);
4057
4058 assert_eq!(
4059 visible_entries_as_strings(&panel, 0..15, cx),
4060 &[
4061 "v root",
4062 " v dir2",
4063 " v subdir2",
4064 " c.txt <== marked",
4065 " d.txt",
4066 " file2.txt <== marked",
4067 " file3.txt <== selected <== marked",
4068 ],
4069 "Initial state before deleting"
4070 );
4071
4072 submit_deletion(&panel, cx);
4073 assert_eq!(
4074 visible_entries_as_strings(&panel, 0..15, cx),
4075 &[
4076 "v root",
4077 " v dir2 <== selected",
4078 " v subdir2",
4079 " d.txt",
4080 ],
4081 "Should select sibling directory"
4082 );
4083}
4084
4085#[gpui::test]
4086async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) {
4087 init_test_with_editor(cx);
4088
4089 let fs = FakeFs::new(cx.executor().clone());
4090 fs.insert_tree(
4091 "/root",
4092 json!({
4093 "dir1": {
4094 "subdir1": {
4095 "a.txt": "",
4096 "b.txt": ""
4097 },
4098 "file1.txt": "",
4099 },
4100 "dir2": {
4101 "subdir2": {
4102 "c.txt": "",
4103 "d.txt": ""
4104 },
4105 "file2.txt": "",
4106 },
4107 "file3.txt": "",
4108 "file4.txt": "",
4109 }),
4110 )
4111 .await;
4112
4113 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4114 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4115 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4116 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4117
4118 toggle_expand_dir(&panel, "root/dir1", cx);
4119 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4120 toggle_expand_dir(&panel, "root/dir2", cx);
4121 toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
4122
4123 // Test Case 1: Select all root files and directories
4124 cx.simulate_modifiers_change(gpui::Modifiers {
4125 control: true,
4126 ..Default::default()
4127 });
4128 select_path_with_mark(&panel, "root/dir1", cx);
4129 select_path_with_mark(&panel, "root/dir2", cx);
4130 select_path_with_mark(&panel, "root/file3.txt", cx);
4131 select_path_with_mark(&panel, "root/file4.txt", cx);
4132 assert_eq!(
4133 visible_entries_as_strings(&panel, 0..20, cx),
4134 &[
4135 "v root",
4136 " v dir1 <== marked",
4137 " v subdir1",
4138 " a.txt",
4139 " b.txt",
4140 " file1.txt",
4141 " v dir2 <== marked",
4142 " v subdir2",
4143 " c.txt",
4144 " d.txt",
4145 " file2.txt",
4146 " file3.txt <== marked",
4147 " file4.txt <== selected <== marked",
4148 ],
4149 "State before deleting all contents"
4150 );
4151
4152 submit_deletion(&panel, cx);
4153 assert_eq!(
4154 visible_entries_as_strings(&panel, 0..20, cx),
4155 &["v root <== selected"],
4156 "Only empty root directory should remain after deleting all contents"
4157 );
4158}
4159
4160#[gpui::test]
4161async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) {
4162 init_test_with_editor(cx);
4163
4164 let fs = FakeFs::new(cx.executor().clone());
4165 fs.insert_tree(
4166 "/root",
4167 json!({
4168 "dir1": {
4169 "subdir1": {
4170 "file_a.txt": "content a",
4171 "file_b.txt": "content b",
4172 },
4173 "subdir2": {
4174 "file_c.txt": "content c",
4175 },
4176 "file1.txt": "content 1",
4177 },
4178 "dir2": {
4179 "file2.txt": "content 2",
4180 },
4181 }),
4182 )
4183 .await;
4184
4185 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4186 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4187 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4188 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4189
4190 toggle_expand_dir(&panel, "root/dir1", cx);
4191 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4192 toggle_expand_dir(&panel, "root/dir2", cx);
4193 cx.simulate_modifiers_change(gpui::Modifiers {
4194 control: true,
4195 ..Default::default()
4196 });
4197
4198 // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory
4199 select_path_with_mark(&panel, "root/dir1", cx);
4200 select_path_with_mark(&panel, "root/dir1/subdir1", cx);
4201 select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx);
4202
4203 assert_eq!(
4204 visible_entries_as_strings(&panel, 0..20, cx),
4205 &[
4206 "v root",
4207 " v dir1 <== marked",
4208 " v subdir1 <== marked",
4209 " file_a.txt <== selected <== marked",
4210 " file_b.txt",
4211 " > subdir2",
4212 " file1.txt",
4213 " v dir2",
4214 " file2.txt",
4215 ],
4216 "State with parent dir, subdir, and file selected"
4217 );
4218 submit_deletion(&panel, cx);
4219 assert_eq!(
4220 visible_entries_as_strings(&panel, 0..20, cx),
4221 &["v root", " v dir2 <== selected", " file2.txt",],
4222 "Only dir2 should remain after deletion"
4223 );
4224}
4225
4226#[gpui::test]
4227async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) {
4228 init_test_with_editor(cx);
4229
4230 let fs = FakeFs::new(cx.executor().clone());
4231 // First worktree
4232 fs.insert_tree(
4233 "/root1",
4234 json!({
4235 "dir1": {
4236 "file1.txt": "content 1",
4237 "file2.txt": "content 2",
4238 },
4239 "dir2": {
4240 "file3.txt": "content 3",
4241 },
4242 }),
4243 )
4244 .await;
4245
4246 // Second worktree
4247 fs.insert_tree(
4248 "/root2",
4249 json!({
4250 "dir3": {
4251 "file4.txt": "content 4",
4252 "file5.txt": "content 5",
4253 },
4254 "file6.txt": "content 6",
4255 }),
4256 )
4257 .await;
4258
4259 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
4260 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4261 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4262 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4263
4264 // Expand all directories for testing
4265 toggle_expand_dir(&panel, "root1/dir1", cx);
4266 toggle_expand_dir(&panel, "root1/dir2", cx);
4267 toggle_expand_dir(&panel, "root2/dir3", cx);
4268
4269 // Test Case 1: Delete files across different worktrees
4270 cx.simulate_modifiers_change(gpui::Modifiers {
4271 control: true,
4272 ..Default::default()
4273 });
4274 select_path_with_mark(&panel, "root1/dir1/file1.txt", cx);
4275 select_path_with_mark(&panel, "root2/dir3/file4.txt", cx);
4276
4277 assert_eq!(
4278 visible_entries_as_strings(&panel, 0..20, cx),
4279 &[
4280 "v root1",
4281 " v dir1",
4282 " file1.txt <== marked",
4283 " file2.txt",
4284 " v dir2",
4285 " file3.txt",
4286 "v root2",
4287 " v dir3",
4288 " file4.txt <== selected <== marked",
4289 " file5.txt",
4290 " file6.txt",
4291 ],
4292 "Initial state with files selected from different worktrees"
4293 );
4294
4295 submit_deletion(&panel, cx);
4296 assert_eq!(
4297 visible_entries_as_strings(&panel, 0..20, cx),
4298 &[
4299 "v root1",
4300 " v dir1",
4301 " file2.txt",
4302 " v dir2",
4303 " file3.txt",
4304 "v root2",
4305 " v dir3",
4306 " file5.txt <== selected",
4307 " file6.txt",
4308 ],
4309 "Should select next file in the last worktree after deletion"
4310 );
4311
4312 // Test Case 2: Delete directories from different worktrees
4313 select_path_with_mark(&panel, "root1/dir1", cx);
4314 select_path_with_mark(&panel, "root2/dir3", cx);
4315
4316 assert_eq!(
4317 visible_entries_as_strings(&panel, 0..20, cx),
4318 &[
4319 "v root1",
4320 " v dir1 <== marked",
4321 " file2.txt",
4322 " v dir2",
4323 " file3.txt",
4324 "v root2",
4325 " v dir3 <== selected <== marked",
4326 " file5.txt",
4327 " file6.txt",
4328 ],
4329 "State with directories marked from different worktrees"
4330 );
4331
4332 submit_deletion(&panel, cx);
4333 assert_eq!(
4334 visible_entries_as_strings(&panel, 0..20, cx),
4335 &[
4336 "v root1",
4337 " v dir2",
4338 " file3.txt",
4339 "v root2",
4340 " file6.txt <== selected",
4341 ],
4342 "Should select remaining file in last worktree after directory deletion"
4343 );
4344
4345 // Test Case 4: Delete all remaining files except roots
4346 select_path_with_mark(&panel, "root1/dir2/file3.txt", cx);
4347 select_path_with_mark(&panel, "root2/file6.txt", cx);
4348
4349 assert_eq!(
4350 visible_entries_as_strings(&panel, 0..20, cx),
4351 &[
4352 "v root1",
4353 " v dir2",
4354 " file3.txt <== marked",
4355 "v root2",
4356 " file6.txt <== selected <== marked",
4357 ],
4358 "State with all remaining files marked"
4359 );
4360
4361 submit_deletion(&panel, cx);
4362 assert_eq!(
4363 visible_entries_as_strings(&panel, 0..20, cx),
4364 &["v root1", " v dir2", "v root2 <== selected"],
4365 "Second parent root should be selected after deleting"
4366 );
4367}
4368
4369#[gpui::test]
4370async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) {
4371 init_test_with_editor(cx);
4372
4373 let fs = FakeFs::new(cx.executor().clone());
4374 fs.insert_tree(
4375 "/root",
4376 json!({
4377 "dir1": {
4378 "file1.txt": "",
4379 "file2.txt": "",
4380 "file3.txt": "",
4381 },
4382 "dir2": {
4383 "file4.txt": "",
4384 "file5.txt": "",
4385 },
4386 }),
4387 )
4388 .await;
4389
4390 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4391 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4392 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4393 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4394
4395 toggle_expand_dir(&panel, "root/dir1", cx);
4396 toggle_expand_dir(&panel, "root/dir2", cx);
4397
4398 cx.simulate_modifiers_change(gpui::Modifiers {
4399 control: true,
4400 ..Default::default()
4401 });
4402
4403 select_path_with_mark(&panel, "root/dir1/file2.txt", cx);
4404 select_path(&panel, "root/dir1/file1.txt", cx);
4405
4406 assert_eq!(
4407 visible_entries_as_strings(&panel, 0..15, cx),
4408 &[
4409 "v root",
4410 " v dir1",
4411 " file1.txt <== selected",
4412 " file2.txt <== marked",
4413 " file3.txt",
4414 " v dir2",
4415 " file4.txt",
4416 " file5.txt",
4417 ],
4418 "Initial state with one marked entry and different selection"
4419 );
4420
4421 // Delete should operate on the selected entry (file1.txt)
4422 submit_deletion(&panel, cx);
4423 assert_eq!(
4424 visible_entries_as_strings(&panel, 0..15, cx),
4425 &[
4426 "v root",
4427 " v dir1",
4428 " file2.txt <== selected <== marked",
4429 " file3.txt",
4430 " v dir2",
4431 " file4.txt",
4432 " file5.txt",
4433 ],
4434 "Should delete selected file, not marked file"
4435 );
4436
4437 select_path_with_mark(&panel, "root/dir1/file3.txt", cx);
4438 select_path_with_mark(&panel, "root/dir2/file4.txt", cx);
4439 select_path(&panel, "root/dir2/file5.txt", cx);
4440
4441 assert_eq!(
4442 visible_entries_as_strings(&panel, 0..15, cx),
4443 &[
4444 "v root",
4445 " v dir1",
4446 " file2.txt <== marked",
4447 " file3.txt <== marked",
4448 " v dir2",
4449 " file4.txt <== marked",
4450 " file5.txt <== selected",
4451 ],
4452 "Initial state with multiple marked entries and different selection"
4453 );
4454
4455 // Delete should operate on all marked entries, ignoring the selection
4456 submit_deletion(&panel, cx);
4457 assert_eq!(
4458 visible_entries_as_strings(&panel, 0..15, cx),
4459 &[
4460 "v root",
4461 " v dir1",
4462 " v dir2",
4463 " file5.txt <== selected",
4464 ],
4465 "Should delete all marked files, leaving only the selected file"
4466 );
4467}
4468
4469#[gpui::test]
4470async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) {
4471 init_test_with_editor(cx);
4472
4473 let fs = FakeFs::new(cx.executor().clone());
4474 fs.insert_tree(
4475 "/root_b",
4476 json!({
4477 "dir1": {
4478 "file1.txt": "content 1",
4479 "file2.txt": "content 2",
4480 },
4481 }),
4482 )
4483 .await;
4484
4485 fs.insert_tree(
4486 "/root_c",
4487 json!({
4488 "dir2": {},
4489 }),
4490 )
4491 .await;
4492
4493 let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await;
4494 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4495 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4496 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4497
4498 toggle_expand_dir(&panel, "root_b/dir1", cx);
4499 toggle_expand_dir(&panel, "root_c/dir2", cx);
4500
4501 cx.simulate_modifiers_change(gpui::Modifiers {
4502 control: true,
4503 ..Default::default()
4504 });
4505 select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx);
4506 select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx);
4507
4508 assert_eq!(
4509 visible_entries_as_strings(&panel, 0..20, cx),
4510 &[
4511 "v root_b",
4512 " v dir1",
4513 " file1.txt <== marked",
4514 " file2.txt <== selected <== marked",
4515 "v root_c",
4516 " v dir2",
4517 ],
4518 "Initial state with files marked in root_b"
4519 );
4520
4521 submit_deletion(&panel, cx);
4522 assert_eq!(
4523 visible_entries_as_strings(&panel, 0..20, cx),
4524 &[
4525 "v root_b",
4526 " v dir1 <== selected",
4527 "v root_c",
4528 " v dir2",
4529 ],
4530 "After deletion in root_b as it's last deletion, selection should be in root_b"
4531 );
4532
4533 select_path_with_mark(&panel, "root_c/dir2", cx);
4534
4535 submit_deletion(&panel, cx);
4536 assert_eq!(
4537 visible_entries_as_strings(&panel, 0..20, cx),
4538 &["v root_b", " v dir1", "v root_c <== selected",],
4539 "After deleting from root_c, it should remain in root_c"
4540 );
4541}
4542
4543fn toggle_expand_dir(
4544 panel: &Entity<ProjectPanel>,
4545 path: impl AsRef<Path>,
4546 cx: &mut VisualTestContext,
4547) {
4548 let path = path.as_ref();
4549 panel.update_in(cx, |panel, window, cx| {
4550 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4551 let worktree = worktree.read(cx);
4552 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4553 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
4554 panel.toggle_expanded(entry_id, window, cx);
4555 return;
4556 }
4557 }
4558 panic!("no worktree for path {:?}", path);
4559 });
4560}
4561
4562#[gpui::test]
4563async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) {
4564 init_test_with_editor(cx);
4565
4566 let fs = FakeFs::new(cx.executor().clone());
4567 fs.insert_tree(
4568 path!("/root"),
4569 json!({
4570 ".gitignore": "**/ignored_dir\n**/ignored_nested",
4571 "dir1": {
4572 "empty1": {
4573 "empty2": {
4574 "empty3": {
4575 "file.txt": ""
4576 }
4577 }
4578 },
4579 "subdir1": {
4580 "file1.txt": "",
4581 "file2.txt": "",
4582 "ignored_nested": {
4583 "ignored_file.txt": ""
4584 }
4585 },
4586 "ignored_dir": {
4587 "subdir": {
4588 "deep_file.txt": ""
4589 }
4590 }
4591 }
4592 }),
4593 )
4594 .await;
4595
4596 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4597 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4598 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4599
4600 // Test 1: When auto-fold is enabled
4601 cx.update(|_, cx| {
4602 let settings = *ProjectPanelSettings::get_global(cx);
4603 ProjectPanelSettings::override_global(
4604 ProjectPanelSettings {
4605 auto_fold_dirs: true,
4606 ..settings
4607 },
4608 cx,
4609 );
4610 });
4611
4612 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4613
4614 assert_eq!(
4615 visible_entries_as_strings(&panel, 0..20, cx),
4616 &["v root", " > dir1", " .gitignore",],
4617 "Initial state should show collapsed root structure"
4618 );
4619
4620 toggle_expand_dir(&panel, "root/dir1", cx);
4621 assert_eq!(
4622 visible_entries_as_strings(&panel, 0..20, cx),
4623 &[
4624 separator!("v root"),
4625 separator!(" v dir1 <== selected"),
4626 separator!(" > empty1/empty2/empty3"),
4627 separator!(" > ignored_dir"),
4628 separator!(" > subdir1"),
4629 separator!(" .gitignore"),
4630 ],
4631 "Should show first level with auto-folded dirs and ignored dir visible"
4632 );
4633
4634 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4635 panel.update(cx, |panel, cx| {
4636 let project = panel.project.read(cx);
4637 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4638 panel.expand_all_for_entry(worktree.id(), entry_id, cx);
4639 panel.update_visible_entries(None, cx);
4640 });
4641 cx.run_until_parked();
4642
4643 assert_eq!(
4644 visible_entries_as_strings(&panel, 0..20, cx),
4645 &[
4646 separator!("v root"),
4647 separator!(" v dir1 <== selected"),
4648 separator!(" v empty1"),
4649 separator!(" v empty2"),
4650 separator!(" v empty3"),
4651 separator!(" file.txt"),
4652 separator!(" > ignored_dir"),
4653 separator!(" v subdir1"),
4654 separator!(" > ignored_nested"),
4655 separator!(" file1.txt"),
4656 separator!(" file2.txt"),
4657 separator!(" .gitignore"),
4658 ],
4659 "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested"
4660 );
4661
4662 // Test 2: When auto-fold is disabled
4663 cx.update(|_, cx| {
4664 let settings = *ProjectPanelSettings::get_global(cx);
4665 ProjectPanelSettings::override_global(
4666 ProjectPanelSettings {
4667 auto_fold_dirs: false,
4668 ..settings
4669 },
4670 cx,
4671 );
4672 });
4673
4674 panel.update_in(cx, |panel, window, cx| {
4675 panel.collapse_all_entries(&CollapseAllEntries, window, cx);
4676 });
4677
4678 toggle_expand_dir(&panel, "root/dir1", cx);
4679 assert_eq!(
4680 visible_entries_as_strings(&panel, 0..20, cx),
4681 &[
4682 separator!("v root"),
4683 separator!(" v dir1 <== selected"),
4684 separator!(" > empty1"),
4685 separator!(" > ignored_dir"),
4686 separator!(" > subdir1"),
4687 separator!(" .gitignore"),
4688 ],
4689 "With auto-fold disabled: should show all directories separately"
4690 );
4691
4692 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4693 panel.update(cx, |panel, cx| {
4694 let project = panel.project.read(cx);
4695 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4696 panel.expand_all_for_entry(worktree.id(), entry_id, cx);
4697 panel.update_visible_entries(None, cx);
4698 });
4699 cx.run_until_parked();
4700
4701 assert_eq!(
4702 visible_entries_as_strings(&panel, 0..20, cx),
4703 &[
4704 separator!("v root"),
4705 separator!(" v dir1 <== selected"),
4706 separator!(" v empty1"),
4707 separator!(" v empty2"),
4708 separator!(" v empty3"),
4709 separator!(" file.txt"),
4710 separator!(" > ignored_dir"),
4711 separator!(" v subdir1"),
4712 separator!(" > ignored_nested"),
4713 separator!(" file1.txt"),
4714 separator!(" file2.txt"),
4715 separator!(" .gitignore"),
4716 ],
4717 "After expand_all without auto-fold: should expand all dirs normally, \
4718 expand ignored_dir itself but not its subdirs, and not expand ignored_nested"
4719 );
4720
4721 // Test 3: When explicitly called on ignored directory
4722 let ignored_dir_entry = find_project_entry(&panel, "root/dir1/ignored_dir", cx).unwrap();
4723 panel.update(cx, |panel, cx| {
4724 let project = panel.project.read(cx);
4725 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4726 panel.expand_all_for_entry(worktree.id(), ignored_dir_entry, cx);
4727 panel.update_visible_entries(None, cx);
4728 });
4729 cx.run_until_parked();
4730
4731 assert_eq!(
4732 visible_entries_as_strings(&panel, 0..20, cx),
4733 &[
4734 separator!("v root"),
4735 separator!(" v dir1 <== selected"),
4736 separator!(" v empty1"),
4737 separator!(" v empty2"),
4738 separator!(" v empty3"),
4739 separator!(" file.txt"),
4740 separator!(" v ignored_dir"),
4741 separator!(" v subdir"),
4742 separator!(" deep_file.txt"),
4743 separator!(" v subdir1"),
4744 separator!(" > ignored_nested"),
4745 separator!(" file1.txt"),
4746 separator!(" file2.txt"),
4747 separator!(" .gitignore"),
4748 ],
4749 "After expand_all on ignored_dir: should expand all contents of the ignored directory"
4750 );
4751}
4752
4753#[gpui::test]
4754async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) {
4755 init_test(cx);
4756
4757 let fs = FakeFs::new(cx.executor().clone());
4758 fs.insert_tree(
4759 path!("/root"),
4760 json!({
4761 "dir1": {
4762 "subdir1": {
4763 "nested1": {
4764 "file1.txt": "",
4765 "file2.txt": ""
4766 },
4767 },
4768 "subdir2": {
4769 "file4.txt": ""
4770 }
4771 },
4772 "dir2": {
4773 "single_file": {
4774 "file5.txt": ""
4775 }
4776 }
4777 }),
4778 )
4779 .await;
4780
4781 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4782 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4783 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4784
4785 // Test 1: Basic collapsing
4786 {
4787 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4788
4789 toggle_expand_dir(&panel, "root/dir1", cx);
4790 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4791 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4792 toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
4793
4794 assert_eq!(
4795 visible_entries_as_strings(&panel, 0..20, cx),
4796 &[
4797 separator!("v root"),
4798 separator!(" v dir1"),
4799 separator!(" v subdir1"),
4800 separator!(" v nested1"),
4801 separator!(" file1.txt"),
4802 separator!(" file2.txt"),
4803 separator!(" v subdir2 <== selected"),
4804 separator!(" file4.txt"),
4805 separator!(" > dir2"),
4806 ],
4807 "Initial state with everything expanded"
4808 );
4809
4810 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4811 panel.update(cx, |panel, cx| {
4812 let project = panel.project.read(cx);
4813 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4814 panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4815 panel.update_visible_entries(None, cx);
4816 });
4817
4818 assert_eq!(
4819 visible_entries_as_strings(&panel, 0..20, cx),
4820 &["v root", " > dir1", " > dir2",],
4821 "All subdirs under dir1 should be collapsed"
4822 );
4823 }
4824
4825 // Test 2: With auto-fold enabled
4826 {
4827 cx.update(|_, cx| {
4828 let settings = *ProjectPanelSettings::get_global(cx);
4829 ProjectPanelSettings::override_global(
4830 ProjectPanelSettings {
4831 auto_fold_dirs: true,
4832 ..settings
4833 },
4834 cx,
4835 );
4836 });
4837
4838 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4839
4840 toggle_expand_dir(&panel, "root/dir1", cx);
4841 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4842 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4843
4844 assert_eq!(
4845 visible_entries_as_strings(&panel, 0..20, cx),
4846 &[
4847 separator!("v root"),
4848 separator!(" v dir1"),
4849 separator!(" v subdir1/nested1 <== selected"),
4850 separator!(" file1.txt"),
4851 separator!(" file2.txt"),
4852 separator!(" > subdir2"),
4853 separator!(" > dir2/single_file"),
4854 ],
4855 "Initial state with some dirs expanded"
4856 );
4857
4858 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4859 panel.update(cx, |panel, cx| {
4860 let project = panel.project.read(cx);
4861 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4862 panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4863 });
4864
4865 toggle_expand_dir(&panel, "root/dir1", cx);
4866
4867 assert_eq!(
4868 visible_entries_as_strings(&panel, 0..20, cx),
4869 &[
4870 separator!("v root"),
4871 separator!(" v dir1 <== selected"),
4872 separator!(" > subdir1/nested1"),
4873 separator!(" > subdir2"),
4874 separator!(" > dir2/single_file"),
4875 ],
4876 "Subdirs should be collapsed and folded with auto-fold enabled"
4877 );
4878 }
4879
4880 // Test 3: With auto-fold disabled
4881 {
4882 cx.update(|_, cx| {
4883 let settings = *ProjectPanelSettings::get_global(cx);
4884 ProjectPanelSettings::override_global(
4885 ProjectPanelSettings {
4886 auto_fold_dirs: false,
4887 ..settings
4888 },
4889 cx,
4890 );
4891 });
4892
4893 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4894
4895 toggle_expand_dir(&panel, "root/dir1", cx);
4896 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4897 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
4898
4899 assert_eq!(
4900 visible_entries_as_strings(&panel, 0..20, cx),
4901 &[
4902 separator!("v root"),
4903 separator!(" v dir1"),
4904 separator!(" v subdir1"),
4905 separator!(" v nested1 <== selected"),
4906 separator!(" file1.txt"),
4907 separator!(" file2.txt"),
4908 separator!(" > subdir2"),
4909 separator!(" > dir2"),
4910 ],
4911 "Initial state with some dirs expanded and auto-fold disabled"
4912 );
4913
4914 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
4915 panel.update(cx, |panel, cx| {
4916 let project = panel.project.read(cx);
4917 let worktree = project.worktrees(cx).next().unwrap().read(cx);
4918 panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
4919 });
4920
4921 toggle_expand_dir(&panel, "root/dir1", cx);
4922
4923 assert_eq!(
4924 visible_entries_as_strings(&panel, 0..20, cx),
4925 &[
4926 separator!("v root"),
4927 separator!(" v dir1 <== selected"),
4928 separator!(" > subdir1"),
4929 separator!(" > subdir2"),
4930 separator!(" > dir2"),
4931 ],
4932 "Subdirs should be collapsed but not folded with auto-fold disabled"
4933 );
4934 }
4935}
4936
4937fn select_path(panel: &Entity<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
4938 let path = path.as_ref();
4939 panel.update(cx, |panel, cx| {
4940 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4941 let worktree = worktree.read(cx);
4942 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4943 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
4944 panel.selection = Some(crate::SelectedEntry {
4945 worktree_id: worktree.id(),
4946 entry_id,
4947 });
4948 return;
4949 }
4950 }
4951 panic!("no worktree for path {:?}", path);
4952 });
4953}
4954
4955fn select_path_with_mark(
4956 panel: &Entity<ProjectPanel>,
4957 path: impl AsRef<Path>,
4958 cx: &mut VisualTestContext,
4959) {
4960 let path = path.as_ref();
4961 panel.update(cx, |panel, cx| {
4962 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4963 let worktree = worktree.read(cx);
4964 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4965 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
4966 let entry = crate::SelectedEntry {
4967 worktree_id: worktree.id(),
4968 entry_id,
4969 };
4970 if !panel.marked_entries.contains(&entry) {
4971 panel.marked_entries.insert(entry);
4972 }
4973 panel.selection = Some(entry);
4974 return;
4975 }
4976 }
4977 panic!("no worktree for path {:?}", path);
4978 });
4979}
4980
4981fn find_project_entry(
4982 panel: &Entity<ProjectPanel>,
4983 path: impl AsRef<Path>,
4984 cx: &mut VisualTestContext,
4985) -> Option<ProjectEntryId> {
4986 let path = path.as_ref();
4987 panel.update(cx, |panel, cx| {
4988 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
4989 let worktree = worktree.read(cx);
4990 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
4991 return worktree.entry_for_path(relative_path).map(|entry| entry.id);
4992 }
4993 }
4994 panic!("no worktree for path {path:?}");
4995 })
4996}
4997
4998fn visible_entries_as_strings(
4999 panel: &Entity<ProjectPanel>,
5000 range: Range<usize>,
5001 cx: &mut VisualTestContext,
5002) -> Vec<String> {
5003 let mut result = Vec::new();
5004 let mut project_entries = HashSet::default();
5005 let mut has_editor = false;
5006
5007 panel.update_in(cx, |panel, window, cx| {
5008 panel.for_each_visible_entry(range, window, cx, |project_entry, details, _, _| {
5009 if details.is_editing {
5010 assert!(!has_editor, "duplicate editor entry");
5011 has_editor = true;
5012 } else {
5013 assert!(
5014 project_entries.insert(project_entry),
5015 "duplicate project entry {:?} {:?}",
5016 project_entry,
5017 details
5018 );
5019 }
5020
5021 let indent = " ".repeat(details.depth);
5022 let icon = if details.kind.is_dir() {
5023 if details.is_expanded { "v " } else { "> " }
5024 } else {
5025 " "
5026 };
5027 let name = if details.is_editing {
5028 format!("[EDITOR: '{}']", details.filename)
5029 } else if details.is_processing {
5030 format!("[PROCESSING: '{}']", details.filename)
5031 } else {
5032 details.filename.clone()
5033 };
5034 let selected = if details.is_selected {
5035 " <== selected"
5036 } else {
5037 ""
5038 };
5039 let marked = if details.is_marked {
5040 " <== marked"
5041 } else {
5042 ""
5043 };
5044
5045 result.push(format!("{indent}{icon}{name}{selected}{marked}"));
5046 });
5047 });
5048
5049 result
5050}
5051
5052fn init_test(cx: &mut TestAppContext) {
5053 cx.update(|cx| {
5054 let settings_store = SettingsStore::test(cx);
5055 cx.set_global(settings_store);
5056 init_settings(cx);
5057 theme::init(theme::LoadThemes::JustBase, cx);
5058 language::init(cx);
5059 editor::init_settings(cx);
5060 crate::init(cx);
5061 workspace::init_settings(cx);
5062 client::init_settings(cx);
5063 Project::init_settings(cx);
5064
5065 cx.update_global::<SettingsStore, _>(|store, cx| {
5066 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5067 project_panel_settings.auto_fold_dirs = Some(false);
5068 });
5069 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5070 worktree_settings.file_scan_exclusions = Some(Vec::new());
5071 });
5072 });
5073 });
5074}
5075
5076fn init_test_with_editor(cx: &mut TestAppContext) {
5077 cx.update(|cx| {
5078 let app_state = AppState::test(cx);
5079 theme::init(theme::LoadThemes::JustBase, cx);
5080 init_settings(cx);
5081 language::init(cx);
5082 editor::init(cx);
5083 crate::init(cx);
5084 workspace::init(app_state.clone(), cx);
5085 Project::init_settings(cx);
5086
5087 cx.update_global::<SettingsStore, _>(|store, cx| {
5088 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5089 project_panel_settings.auto_fold_dirs = Some(false);
5090 });
5091 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5092 worktree_settings.file_scan_exclusions = Some(Vec::new());
5093 });
5094 });
5095 });
5096}
5097
5098fn ensure_single_file_is_opened(
5099 window: &WindowHandle<Workspace>,
5100 expected_path: &str,
5101 cx: &mut TestAppContext,
5102) {
5103 window
5104 .update(cx, |workspace, _, cx| {
5105 let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
5106 assert_eq!(worktrees.len(), 1);
5107 let worktree_id = worktrees[0].read(cx).id();
5108
5109 let open_project_paths = workspace
5110 .panes()
5111 .iter()
5112 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
5113 .collect::<Vec<_>>();
5114 assert_eq!(
5115 open_project_paths,
5116 vec![ProjectPath {
5117 worktree_id,
5118 path: Arc::from(Path::new(expected_path))
5119 }],
5120 "Should have opened file, selected in project panel"
5121 );
5122 })
5123 .unwrap();
5124}
5125
5126fn submit_deletion(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
5127 assert!(
5128 !cx.has_pending_prompt(),
5129 "Should have no prompts before the deletion"
5130 );
5131 panel.update_in(cx, |panel, window, cx| {
5132 panel.delete(&Delete { skip_prompt: false }, window, cx)
5133 });
5134 assert!(
5135 cx.has_pending_prompt(),
5136 "Should have a prompt after the deletion"
5137 );
5138 cx.simulate_prompt_answer("Delete");
5139 assert!(
5140 !cx.has_pending_prompt(),
5141 "Should have no prompts after prompt was replied to"
5142 );
5143 cx.executor().run_until_parked();
5144}
5145
5146fn submit_deletion_skipping_prompt(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
5147 assert!(
5148 !cx.has_pending_prompt(),
5149 "Should have no prompts before the deletion"
5150 );
5151 panel.update_in(cx, |panel, window, cx| {
5152 panel.delete(&Delete { skip_prompt: true }, window, cx)
5153 });
5154 assert!(!cx.has_pending_prompt(), "Should have received no prompts");
5155 cx.executor().run_until_parked();
5156}
5157
5158fn ensure_no_open_items_and_panes(workspace: &WindowHandle<Workspace>, cx: &mut VisualTestContext) {
5159 assert!(
5160 !cx.has_pending_prompt(),
5161 "Should have no prompts after deletion operation closes the file"
5162 );
5163 workspace
5164 .read_with(cx, |workspace, cx| {
5165 let open_project_paths = workspace
5166 .panes()
5167 .iter()
5168 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
5169 .collect::<Vec<_>>();
5170 assert!(
5171 open_project_paths.is_empty(),
5172 "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
5173 );
5174 })
5175 .unwrap();
5176}
5177
5178struct TestProjectItemView {
5179 focus_handle: FocusHandle,
5180 path: ProjectPath,
5181}
5182
5183struct TestProjectItem {
5184 path: ProjectPath,
5185}
5186
5187impl project::ProjectItem for TestProjectItem {
5188 fn try_open(
5189 _project: &Entity<Project>,
5190 path: &ProjectPath,
5191 cx: &mut App,
5192 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
5193 let path = path.clone();
5194 Some(cx.spawn(async move |cx| cx.new(|_| Self { path })))
5195 }
5196
5197 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
5198 None
5199 }
5200
5201 fn project_path(&self, _: &App) -> Option<ProjectPath> {
5202 Some(self.path.clone())
5203 }
5204
5205 fn is_dirty(&self) -> bool {
5206 false
5207 }
5208}
5209
5210impl ProjectItem for TestProjectItemView {
5211 type Item = TestProjectItem;
5212
5213 fn for_project_item(
5214 _: Entity<Project>,
5215 _: &Pane,
5216 project_item: Entity<Self::Item>,
5217 _: &mut Window,
5218 cx: &mut Context<Self>,
5219 ) -> Self
5220 where
5221 Self: Sized,
5222 {
5223 Self {
5224 path: project_item.update(cx, |project_item, _| project_item.path.clone()),
5225 focus_handle: cx.focus_handle(),
5226 }
5227 }
5228}
5229
5230impl Item for TestProjectItemView {
5231 type Event = ();
5232}
5233
5234impl EventEmitter<()> for TestProjectItemView {}
5235
5236impl Focusable for TestProjectItemView {
5237 fn focus_handle(&self, _: &App) -> FocusHandle {
5238 self.focus_handle.clone()
5239 }
5240}
5241
5242impl Render for TestProjectItemView {
5243 fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5244 Empty
5245 }
5246}