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