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;
10use workspace::{
11 AppState, ItemHandle, 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());
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());
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());
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 // Test 1: Multiple worktrees with auto_fold_dirs = true
313 let project = Project::test(
314 fs.clone(),
315 [path!("/root1").as_ref(), path!("/root2").as_ref()],
316 cx,
317 )
318 .await;
319 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
320 let cx = &mut VisualTestContext::from_window(*workspace, cx);
321 cx.update(|_, cx| {
322 let settings = *ProjectPanelSettings::get_global(cx);
323 ProjectPanelSettings::override_global(
324 ProjectPanelSettings {
325 auto_fold_dirs: true,
326 ..settings
327 },
328 cx,
329 );
330 });
331 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
332 assert_eq!(
333 visible_entries_as_strings(&panel, 0..10, cx),
334 &[
335 "v root1",
336 " > dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
337 "v root2",
338 " > dir_2",
339 ]
340 );
341
342 toggle_expand_dir(
343 &panel,
344 "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
345 cx,
346 );
347 assert_eq!(
348 visible_entries_as_strings(&panel, 0..10, cx),
349 &[
350 "v root1",
351 " v dir_1/nested_dir_1/nested_dir_2/nested_dir_3 <== selected",
352 " > nested_dir_4/nested_dir_5",
353 " file_a.java",
354 " file_b.java",
355 " file_c.java",
356 "v root2",
357 " > dir_2",
358 ]
359 );
360
361 toggle_expand_dir(
362 &panel,
363 "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
364 cx,
365 );
366 assert_eq!(
367 visible_entries_as_strings(&panel, 0..10, cx),
368 &[
369 "v root1",
370 " v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
371 " v nested_dir_4/nested_dir_5 <== selected",
372 " file_d.java",
373 " file_a.java",
374 " file_b.java",
375 " file_c.java",
376 "v root2",
377 " > dir_2",
378 ]
379 );
380 toggle_expand_dir(&panel, "root2/dir_2", cx);
381 assert_eq!(
382 visible_entries_as_strings(&panel, 0..10, cx),
383 &[
384 "v root1",
385 " v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
386 " v nested_dir_4/nested_dir_5",
387 " file_d.java",
388 " file_a.java",
389 " file_b.java",
390 " file_c.java",
391 "v root2",
392 " v dir_2 <== selected",
393 " file_1.java",
394 ]
395 );
396
397 // Test 2: Single worktree with auto_fold_dirs = true and hide_root = true
398 {
399 let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await;
400 let workspace =
401 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
402 let cx = &mut VisualTestContext::from_window(*workspace, cx);
403 cx.update(|_, cx| {
404 let settings = *ProjectPanelSettings::get_global(cx);
405 ProjectPanelSettings::override_global(
406 ProjectPanelSettings {
407 auto_fold_dirs: true,
408 hide_root: true,
409 ..settings
410 },
411 cx,
412 );
413 });
414 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
415 assert_eq!(
416 visible_entries_as_strings(&panel, 0..10, cx),
417 &["> dir_1/nested_dir_1/nested_dir_2/nested_dir_3"],
418 "Single worktree with hide_root=true should hide root and show auto-folded paths"
419 );
420
421 toggle_expand_dir(
422 &panel,
423 "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
424 cx,
425 );
426 assert_eq!(
427 visible_entries_as_strings(&panel, 0..10, cx),
428 &[
429 "v dir_1/nested_dir_1/nested_dir_2/nested_dir_3 <== selected",
430 " > nested_dir_4/nested_dir_5",
431 " file_a.java",
432 " file_b.java",
433 " file_c.java",
434 ],
435 "Expanded auto-folded path with hidden root should show contents without root prefix"
436 );
437
438 toggle_expand_dir(
439 &panel,
440 "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
441 cx,
442 );
443 assert_eq!(
444 visible_entries_as_strings(&panel, 0..10, cx),
445 &[
446 "v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
447 " v nested_dir_4/nested_dir_5 <== selected",
448 " file_d.java",
449 " file_a.java",
450 " file_b.java",
451 " file_c.java",
452 ],
453 "Nested expansion with hidden root should maintain proper indentation"
454 );
455 }
456}
457
458#[gpui::test(iterations = 30)]
459async fn test_editing_files(cx: &mut gpui::TestAppContext) {
460 init_test(cx);
461
462 let fs = FakeFs::new(cx.executor());
463 fs.insert_tree(
464 "/root1",
465 json!({
466 ".dockerignore": "",
467 ".git": {
468 "HEAD": "",
469 },
470 "a": {
471 "0": { "q": "", "r": "", "s": "" },
472 "1": { "t": "", "u": "" },
473 "2": { "v": "", "w": "", "x": "", "y": "" },
474 },
475 "b": {
476 "3": { "Q": "" },
477 "4": { "R": "", "S": "", "T": "", "U": "" },
478 },
479 "C": {
480 "5": {},
481 "6": { "V": "", "W": "" },
482 "7": { "X": "" },
483 "8": { "Y": {}, "Z": "" }
484 }
485 }),
486 )
487 .await;
488 fs.insert_tree(
489 "/root2",
490 json!({
491 "d": {
492 "9": ""
493 },
494 "e": {}
495 }),
496 )
497 .await;
498
499 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
500 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
501 let cx = &mut VisualTestContext::from_window(*workspace, cx);
502 let panel = workspace
503 .update(cx, |workspace, window, cx| {
504 let panel = ProjectPanel::new(workspace, window, cx);
505 workspace.add_panel(panel.clone(), window, cx);
506 panel
507 })
508 .unwrap();
509
510 select_path(&panel, "root1", cx);
511 assert_eq!(
512 visible_entries_as_strings(&panel, 0..10, cx),
513 &[
514 "v root1 <== selected",
515 " > .git",
516 " > a",
517 " > b",
518 " > C",
519 " .dockerignore",
520 "v root2",
521 " > d",
522 " > e",
523 ]
524 );
525
526 // Add a file with the root folder selected. The filename editor is placed
527 // before the first file in the root folder.
528 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
529 panel.update_in(cx, |panel, window, cx| {
530 assert!(panel.filename_editor.read(cx).is_focused(window));
531 });
532 assert_eq!(
533 visible_entries_as_strings(&panel, 0..10, cx),
534 &[
535 "v root1",
536 " > .git",
537 " > a",
538 " > b",
539 " > C",
540 " [EDITOR: ''] <== selected",
541 " .dockerignore",
542 "v root2",
543 " > d",
544 " > e",
545 ]
546 );
547
548 let confirm = panel.update_in(cx, |panel, window, cx| {
549 panel.filename_editor.update(cx, |editor, cx| {
550 editor.set_text("the-new-filename", window, cx)
551 });
552 panel.confirm_edit(window, cx).unwrap()
553 });
554 assert_eq!(
555 visible_entries_as_strings(&panel, 0..10, cx),
556 &[
557 "v root1",
558 " > .git",
559 " > a",
560 " > b",
561 " > C",
562 " [PROCESSING: 'the-new-filename'] <== selected",
563 " .dockerignore",
564 "v root2",
565 " > d",
566 " > e",
567 ]
568 );
569
570 confirm.await.unwrap();
571 assert_eq!(
572 visible_entries_as_strings(&panel, 0..10, cx),
573 &[
574 "v root1",
575 " > .git",
576 " > a",
577 " > b",
578 " > C",
579 " .dockerignore",
580 " the-new-filename <== selected <== marked",
581 "v root2",
582 " > d",
583 " > e",
584 ]
585 );
586
587 select_path(&panel, "root1/b", cx);
588 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
589 assert_eq!(
590 visible_entries_as_strings(&panel, 0..10, cx),
591 &[
592 "v root1",
593 " > .git",
594 " > a",
595 " v b",
596 " > 3",
597 " > 4",
598 " [EDITOR: ''] <== selected",
599 " > C",
600 " .dockerignore",
601 " the-new-filename",
602 ]
603 );
604
605 panel
606 .update_in(cx, |panel, window, cx| {
607 panel.filename_editor.update(cx, |editor, cx| {
608 editor.set_text("another-filename.txt", window, cx)
609 });
610 panel.confirm_edit(window, cx).unwrap()
611 })
612 .await
613 .unwrap();
614 assert_eq!(
615 visible_entries_as_strings(&panel, 0..10, cx),
616 &[
617 "v root1",
618 " > .git",
619 " > a",
620 " v b",
621 " > 3",
622 " > 4",
623 " another-filename.txt <== selected <== marked",
624 " > C",
625 " .dockerignore",
626 " the-new-filename",
627 ]
628 );
629
630 select_path(&panel, "root1/b/another-filename.txt", cx);
631 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
632 assert_eq!(
633 visible_entries_as_strings(&panel, 0..10, cx),
634 &[
635 "v root1",
636 " > .git",
637 " > a",
638 " v b",
639 " > 3",
640 " > 4",
641 " [EDITOR: 'another-filename.txt'] <== selected <== marked",
642 " > C",
643 " .dockerignore",
644 " the-new-filename",
645 ]
646 );
647
648 let confirm = panel.update_in(cx, |panel, window, cx| {
649 panel.filename_editor.update(cx, |editor, cx| {
650 let file_name_selections = editor.selections.all::<usize>(cx);
651 assert_eq!(
652 file_name_selections.len(),
653 1,
654 "File editing should have a single selection, but got: {file_name_selections:?}"
655 );
656 let file_name_selection = &file_name_selections[0];
657 assert_eq!(
658 file_name_selection.start, 0,
659 "Should select the file name from the start"
660 );
661 assert_eq!(
662 file_name_selection.end,
663 "another-filename".len(),
664 "Should not select file extension"
665 );
666
667 editor.set_text("a-different-filename.tar.gz", window, cx)
668 });
669 panel.confirm_edit(window, cx).unwrap()
670 });
671 assert_eq!(
672 visible_entries_as_strings(&panel, 0..10, cx),
673 &[
674 "v root1",
675 " > .git",
676 " > a",
677 " v b",
678 " > 3",
679 " > 4",
680 " [PROCESSING: 'a-different-filename.tar.gz'] <== selected <== marked",
681 " > C",
682 " .dockerignore",
683 " the-new-filename",
684 ]
685 );
686
687 confirm.await.unwrap();
688 assert_eq!(
689 visible_entries_as_strings(&panel, 0..10, cx),
690 &[
691 "v root1",
692 " > .git",
693 " > a",
694 " v b",
695 " > 3",
696 " > 4",
697 " a-different-filename.tar.gz <== selected",
698 " > C",
699 " .dockerignore",
700 " the-new-filename",
701 ]
702 );
703
704 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
705 assert_eq!(
706 visible_entries_as_strings(&panel, 0..10, cx),
707 &[
708 "v root1",
709 " > .git",
710 " > a",
711 " v b",
712 " > 3",
713 " > 4",
714 " [EDITOR: 'a-different-filename.tar.gz'] <== selected",
715 " > C",
716 " .dockerignore",
717 " the-new-filename",
718 ]
719 );
720
721 panel.update_in(cx, |panel, window, cx| {
722 panel.filename_editor.update(cx, |editor, cx| {
723 let file_name_selections = editor.selections.all::<usize>(cx);
724 assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
725 let file_name_selection = &file_name_selections[0];
726 assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
727 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..");
728
729 });
730 panel.cancel(&menu::Cancel, window, cx)
731 });
732
733 panel.update_in(cx, |panel, window, cx| {
734 panel.new_directory(&NewDirectory, 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 " > [EDITOR: ''] <== selected",
744 " > 3",
745 " > 4",
746 " a-different-filename.tar.gz",
747 " > C",
748 " .dockerignore",
749 ]
750 );
751
752 let confirm = panel.update_in(cx, |panel, window, cx| {
753 panel
754 .filename_editor
755 .update(cx, |editor, cx| editor.set_text("new-dir", window, cx));
756 panel.confirm_edit(window, cx).unwrap()
757 });
758 panel.update_in(cx, |panel, window, cx| {
759 panel.select_next(&Default::default(), window, cx)
760 });
761 assert_eq!(
762 visible_entries_as_strings(&panel, 0..10, cx),
763 &[
764 "v root1",
765 " > .git",
766 " > a",
767 " v b",
768 " > [PROCESSING: 'new-dir']",
769 " > 3 <== selected",
770 " > 4",
771 " a-different-filename.tar.gz",
772 " > C",
773 " .dockerignore",
774 ]
775 );
776
777 confirm.await.unwrap();
778 assert_eq!(
779 visible_entries_as_strings(&panel, 0..10, cx),
780 &[
781 "v root1",
782 " > .git",
783 " > a",
784 " v b",
785 " > 3 <== selected",
786 " > 4",
787 " > new-dir",
788 " a-different-filename.tar.gz",
789 " > C",
790 " .dockerignore",
791 ]
792 );
793
794 panel.update_in(cx, |panel, window, cx| {
795 panel.rename(&Default::default(), window, cx)
796 });
797 assert_eq!(
798 visible_entries_as_strings(&panel, 0..10, cx),
799 &[
800 "v root1",
801 " > .git",
802 " > a",
803 " v b",
804 " > [EDITOR: '3'] <== selected",
805 " > 4",
806 " > new-dir",
807 " a-different-filename.tar.gz",
808 " > C",
809 " .dockerignore",
810 ]
811 );
812
813 // Dismiss the rename editor when it loses focus.
814 workspace.update(cx, |_, window, _| window.blur()).unwrap();
815 assert_eq!(
816 visible_entries_as_strings(&panel, 0..10, cx),
817 &[
818 "v root1",
819 " > .git",
820 " > a",
821 " v b",
822 " > 3 <== selected",
823 " > 4",
824 " > new-dir",
825 " a-different-filename.tar.gz",
826 " > C",
827 " .dockerignore",
828 ]
829 );
830
831 // Test empty filename and filename with only whitespace
832 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
833 assert_eq!(
834 visible_entries_as_strings(&panel, 0..10, cx),
835 &[
836 "v root1",
837 " > .git",
838 " > a",
839 " v b",
840 " v 3",
841 " [EDITOR: ''] <== selected",
842 " Q",
843 " > 4",
844 " > new-dir",
845 " a-different-filename.tar.gz",
846 ]
847 );
848 panel.update_in(cx, |panel, window, cx| {
849 panel.filename_editor.update(cx, |editor, cx| {
850 editor.set_text("", window, cx);
851 });
852 assert!(panel.confirm_edit(window, cx).is_none());
853 panel.filename_editor.update(cx, |editor, cx| {
854 editor.set_text(" ", window, cx);
855 });
856 assert!(panel.confirm_edit(window, cx).is_none());
857 panel.cancel(&menu::Cancel, window, cx)
858 });
859 assert_eq!(
860 visible_entries_as_strings(&panel, 0..10, cx),
861 &[
862 "v root1",
863 " > .git",
864 " > a",
865 " v b",
866 " v 3 <== selected",
867 " Q",
868 " > 4",
869 " > new-dir",
870 " a-different-filename.tar.gz",
871 " > C",
872 ]
873 );
874}
875
876#[gpui::test(iterations = 10)]
877async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
878 init_test(cx);
879
880 let fs = FakeFs::new(cx.executor());
881 fs.insert_tree(
882 "/root1",
883 json!({
884 ".dockerignore": "",
885 ".git": {
886 "HEAD": "",
887 },
888 "a": {
889 "0": { "q": "", "r": "", "s": "" },
890 "1": { "t": "", "u": "" },
891 "2": { "v": "", "w": "", "x": "", "y": "" },
892 },
893 "b": {
894 "3": { "Q": "" },
895 "4": { "R": "", "S": "", "T": "", "U": "" },
896 },
897 "C": {
898 "5": {},
899 "6": { "V": "", "W": "" },
900 "7": { "X": "" },
901 "8": { "Y": {}, "Z": "" }
902 }
903 }),
904 )
905 .await;
906 fs.insert_tree(
907 "/root2",
908 json!({
909 "d": {
910 "9": ""
911 },
912 "e": {}
913 }),
914 )
915 .await;
916
917 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
918 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
919 let cx = &mut VisualTestContext::from_window(*workspace, cx);
920 let panel = workspace
921 .update(cx, |workspace, window, cx| {
922 let panel = ProjectPanel::new(workspace, window, cx);
923 workspace.add_panel(panel.clone(), window, cx);
924 panel
925 })
926 .unwrap();
927
928 select_path(&panel, "root1", cx);
929 assert_eq!(
930 visible_entries_as_strings(&panel, 0..10, cx),
931 &[
932 "v root1 <== selected",
933 " > .git",
934 " > a",
935 " > b",
936 " > C",
937 " .dockerignore",
938 "v root2",
939 " > d",
940 " > e",
941 ]
942 );
943
944 // Add a file with the root folder selected. The filename editor is placed
945 // before the first file in the root folder.
946 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
947 panel.update_in(cx, |panel, window, cx| {
948 assert!(panel.filename_editor.read(cx).is_focused(window));
949 });
950 assert_eq!(
951 visible_entries_as_strings(&panel, 0..10, cx),
952 &[
953 "v root1",
954 " > .git",
955 " > a",
956 " > b",
957 " > C",
958 " [EDITOR: ''] <== selected",
959 " .dockerignore",
960 "v root2",
961 " > d",
962 " > e",
963 ]
964 );
965
966 let confirm = panel.update_in(cx, |panel, window, cx| {
967 panel.filename_editor.update(cx, |editor, cx| {
968 editor.set_text("/bdir1/dir2/the-new-filename", window, cx)
969 });
970 panel.confirm_edit(window, cx).unwrap()
971 });
972
973 assert_eq!(
974 visible_entries_as_strings(&panel, 0..10, cx),
975 &[
976 "v root1",
977 " > .git",
978 " > a",
979 " > b",
980 " > C",
981 " [PROCESSING: '/bdir1/dir2/the-new-filename'] <== selected",
982 " .dockerignore",
983 "v root2",
984 " > d",
985 " > e",
986 ]
987 );
988
989 confirm.await.unwrap();
990 assert_eq!(
991 visible_entries_as_strings(&panel, 0..13, cx),
992 &[
993 "v root1",
994 " > .git",
995 " > a",
996 " > b",
997 " v bdir1",
998 " v dir2",
999 " the-new-filename <== selected <== marked",
1000 " > C",
1001 " .dockerignore",
1002 "v root2",
1003 " > d",
1004 " > e",
1005 ]
1006 );
1007}
1008
1009#[gpui::test]
1010async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) {
1011 init_test(cx);
1012
1013 let fs = FakeFs::new(cx.executor());
1014 fs.insert_tree(
1015 path!("/root1"),
1016 json!({
1017 ".dockerignore": "",
1018 ".git": {
1019 "HEAD": "",
1020 },
1021 }),
1022 )
1023 .await;
1024
1025 let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await;
1026 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1027 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1028 let panel = workspace
1029 .update(cx, |workspace, window, cx| {
1030 let panel = ProjectPanel::new(workspace, window, cx);
1031 workspace.add_panel(panel.clone(), window, cx);
1032 panel
1033 })
1034 .unwrap();
1035
1036 select_path(&panel, "root1", cx);
1037 assert_eq!(
1038 visible_entries_as_strings(&panel, 0..10, cx),
1039 &["v root1 <== selected", " > .git", " .dockerignore",]
1040 );
1041
1042 // Add a file with the root folder selected. The filename editor is placed
1043 // before the first file in the root folder.
1044 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
1045 panel.update_in(cx, |panel, window, cx| {
1046 assert!(panel.filename_editor.read(cx).is_focused(window));
1047 });
1048 assert_eq!(
1049 visible_entries_as_strings(&panel, 0..10, cx),
1050 &[
1051 "v root1",
1052 " > .git",
1053 " [EDITOR: ''] <== selected",
1054 " .dockerignore",
1055 ]
1056 );
1057
1058 let confirm = panel.update_in(cx, |panel, window, cx| {
1059 // If we want to create a subdirectory, there should be no prefix slash.
1060 panel
1061 .filename_editor
1062 .update(cx, |editor, cx| editor.set_text("new_dir/", window, cx));
1063 panel.confirm_edit(window, cx).unwrap()
1064 });
1065
1066 assert_eq!(
1067 visible_entries_as_strings(&panel, 0..10, cx),
1068 &[
1069 "v root1",
1070 " > .git",
1071 " [PROCESSING: 'new_dir/'] <== selected",
1072 " .dockerignore",
1073 ]
1074 );
1075
1076 confirm.await.unwrap();
1077 assert_eq!(
1078 visible_entries_as_strings(&panel, 0..10, cx),
1079 &[
1080 "v root1",
1081 " > .git",
1082 " v new_dir <== selected",
1083 " .dockerignore",
1084 ]
1085 );
1086
1087 // Test filename with whitespace
1088 select_path(&panel, "root1", cx);
1089 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
1090 let confirm = panel.update_in(cx, |panel, window, cx| {
1091 // If we want to create a subdirectory, there should be no prefix slash.
1092 panel
1093 .filename_editor
1094 .update(cx, |editor, cx| editor.set_text("new dir 2/", window, cx));
1095 panel.confirm_edit(window, cx).unwrap()
1096 });
1097 confirm.await.unwrap();
1098 assert_eq!(
1099 visible_entries_as_strings(&panel, 0..10, cx),
1100 &[
1101 "v root1",
1102 " > .git",
1103 " v new dir 2 <== selected",
1104 " v new_dir",
1105 " .dockerignore",
1106 ]
1107 );
1108
1109 // Test filename ends with "\"
1110 #[cfg(target_os = "windows")]
1111 {
1112 select_path(&panel, "root1", cx);
1113 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
1114 let confirm = panel.update_in(cx, |panel, window, cx| {
1115 // If we want to create a subdirectory, there should be no prefix slash.
1116 panel
1117 .filename_editor
1118 .update(cx, |editor, cx| editor.set_text("new_dir_3\\", window, cx));
1119 panel.confirm_edit(window, cx).unwrap()
1120 });
1121 confirm.await.unwrap();
1122 assert_eq!(
1123 visible_entries_as_strings(&panel, 0..10, cx),
1124 &[
1125 "v root1",
1126 " > .git",
1127 " v new dir 2",
1128 " v new_dir",
1129 " v new_dir_3 <== selected",
1130 " .dockerignore",
1131 ]
1132 );
1133 }
1134}
1135
1136#[gpui::test]
1137async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
1138 init_test(cx);
1139
1140 let fs = FakeFs::new(cx.executor());
1141 fs.insert_tree(
1142 "/root1",
1143 json!({
1144 "one.two.txt": "",
1145 "one.txt": ""
1146 }),
1147 )
1148 .await;
1149
1150 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
1151 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1152 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1153 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1154
1155 panel.update_in(cx, |panel, window, cx| {
1156 panel.select_next(&Default::default(), window, cx);
1157 panel.select_next(&Default::default(), window, cx);
1158 });
1159
1160 assert_eq!(
1161 visible_entries_as_strings(&panel, 0..50, cx),
1162 &[
1163 //
1164 "v root1",
1165 " one.txt <== selected",
1166 " one.two.txt",
1167 ]
1168 );
1169
1170 // Regression test - file name is created correctly when
1171 // the copied file's name contains multiple dots.
1172 panel.update_in(cx, |panel, window, cx| {
1173 panel.copy(&Default::default(), window, cx);
1174 panel.paste(&Default::default(), window, cx);
1175 });
1176 cx.executor().run_until_parked();
1177
1178 assert_eq!(
1179 visible_entries_as_strings(&panel, 0..50, cx),
1180 &[
1181 //
1182 "v root1",
1183 " one.txt",
1184 " [EDITOR: 'one copy.txt'] <== selected <== marked",
1185 " one.two.txt",
1186 ]
1187 );
1188
1189 panel.update_in(cx, |panel, window, cx| {
1190 panel.filename_editor.update(cx, |editor, cx| {
1191 let file_name_selections = editor.selections.all::<usize>(cx);
1192 assert_eq!(
1193 file_name_selections.len(),
1194 1,
1195 "File editing should have a single selection, but got: {file_name_selections:?}"
1196 );
1197 let file_name_selection = &file_name_selections[0];
1198 assert_eq!(
1199 file_name_selection.start,
1200 "one".len(),
1201 "Should select the file name disambiguation after the original file name"
1202 );
1203 assert_eq!(
1204 file_name_selection.end,
1205 "one copy".len(),
1206 "Should select the file name disambiguation until the extension"
1207 );
1208 });
1209 assert!(panel.confirm_edit(window, cx).is_none());
1210 });
1211
1212 panel.update_in(cx, |panel, window, cx| {
1213 panel.paste(&Default::default(), window, cx);
1214 });
1215 cx.executor().run_until_parked();
1216
1217 assert_eq!(
1218 visible_entries_as_strings(&panel, 0..50, cx),
1219 &[
1220 //
1221 "v root1",
1222 " one.txt",
1223 " one copy.txt",
1224 " [EDITOR: 'one copy 1.txt'] <== selected <== marked",
1225 " one.two.txt",
1226 ]
1227 );
1228
1229 panel.update_in(cx, |panel, window, cx| {
1230 assert!(panel.confirm_edit(window, cx).is_none())
1231 });
1232}
1233
1234#[gpui::test]
1235async fn test_cut_paste(cx: &mut gpui::TestAppContext) {
1236 init_test(cx);
1237
1238 let fs = FakeFs::new(cx.executor());
1239 fs.insert_tree(
1240 "/root",
1241 json!({
1242 "one.txt": "",
1243 "two.txt": "",
1244 "a": {},
1245 "b": {}
1246 }),
1247 )
1248 .await;
1249
1250 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
1251 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1252 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1253 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1254
1255 select_path_with_mark(&panel, "root/one.txt", cx);
1256 select_path_with_mark(&panel, "root/two.txt", cx);
1257
1258 assert_eq!(
1259 visible_entries_as_strings(&panel, 0..50, cx),
1260 &[
1261 "v root",
1262 " > a",
1263 " > b",
1264 " one.txt <== marked",
1265 " two.txt <== selected <== marked",
1266 ]
1267 );
1268
1269 panel.update_in(cx, |panel, window, cx| {
1270 panel.cut(&Default::default(), window, cx);
1271 });
1272
1273 select_path(&panel, "root/a", cx);
1274
1275 panel.update_in(cx, |panel, window, cx| {
1276 panel.paste(&Default::default(), window, cx);
1277 });
1278 cx.executor().run_until_parked();
1279
1280 assert_eq!(
1281 visible_entries_as_strings(&panel, 0..50, cx),
1282 &[
1283 "v root",
1284 " v a",
1285 " one.txt <== marked",
1286 " two.txt <== selected <== marked",
1287 " > b",
1288 ],
1289 "Cut entries should be moved on first paste."
1290 );
1291
1292 panel.update_in(cx, |panel, window, cx| {
1293 panel.cancel(&menu::Cancel {}, window, cx)
1294 });
1295 cx.executor().run_until_parked();
1296
1297 select_path(&panel, "root/b", cx);
1298
1299 panel.update_in(cx, |panel, window, cx| {
1300 panel.paste(&Default::default(), window, cx);
1301 });
1302 cx.executor().run_until_parked();
1303
1304 assert_eq!(
1305 visible_entries_as_strings(&panel, 0..50, cx),
1306 &[
1307 "v root",
1308 " v a",
1309 " one.txt",
1310 " two.txt",
1311 " v b",
1312 " one.txt",
1313 " two.txt <== selected",
1314 ],
1315 "Cut entries should only be copied for the second paste!"
1316 );
1317}
1318
1319#[gpui::test]
1320async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
1321 init_test(cx);
1322
1323 let fs = FakeFs::new(cx.executor());
1324 fs.insert_tree(
1325 "/root1",
1326 json!({
1327 "one.txt": "",
1328 "two.txt": "",
1329 "three.txt": "",
1330 "a": {
1331 "0": { "q": "", "r": "", "s": "" },
1332 "1": { "t": "", "u": "" },
1333 "2": { "v": "", "w": "", "x": "", "y": "" },
1334 },
1335 }),
1336 )
1337 .await;
1338
1339 fs.insert_tree(
1340 "/root2",
1341 json!({
1342 "one.txt": "",
1343 "two.txt": "",
1344 "four.txt": "",
1345 "b": {
1346 "3": { "Q": "" },
1347 "4": { "R": "", "S": "", "T": "", "U": "" },
1348 },
1349 }),
1350 )
1351 .await;
1352
1353 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1354 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1355 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1356 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1357
1358 select_path(&panel, "root1/three.txt", cx);
1359 panel.update_in(cx, |panel, window, cx| {
1360 panel.cut(&Default::default(), window, cx);
1361 });
1362
1363 select_path(&panel, "root2/one.txt", cx);
1364 panel.update_in(cx, |panel, window, cx| {
1365 panel.select_next(&Default::default(), window, cx);
1366 panel.paste(&Default::default(), window, cx);
1367 });
1368 cx.executor().run_until_parked();
1369 assert_eq!(
1370 visible_entries_as_strings(&panel, 0..50, cx),
1371 &[
1372 //
1373 "v root1",
1374 " > a",
1375 " one.txt",
1376 " two.txt",
1377 "v root2",
1378 " > b",
1379 " four.txt",
1380 " one.txt",
1381 " three.txt <== selected <== marked",
1382 " two.txt",
1383 ]
1384 );
1385
1386 select_path(&panel, "root1/a", cx);
1387 panel.update_in(cx, |panel, window, cx| {
1388 panel.cut(&Default::default(), window, cx);
1389 });
1390 select_path(&panel, "root2/two.txt", cx);
1391 panel.update_in(cx, |panel, window, cx| {
1392 panel.select_next(&Default::default(), window, cx);
1393 panel.paste(&Default::default(), window, cx);
1394 });
1395
1396 cx.executor().run_until_parked();
1397 assert_eq!(
1398 visible_entries_as_strings(&panel, 0..50, cx),
1399 &[
1400 //
1401 "v root1",
1402 " one.txt",
1403 " two.txt",
1404 "v root2",
1405 " > a <== selected",
1406 " > b",
1407 " four.txt",
1408 " one.txt",
1409 " three.txt <== marked",
1410 " two.txt",
1411 ]
1412 );
1413}
1414
1415#[gpui::test]
1416async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
1417 init_test(cx);
1418
1419 let fs = FakeFs::new(cx.executor());
1420 fs.insert_tree(
1421 "/root1",
1422 json!({
1423 "one.txt": "",
1424 "two.txt": "",
1425 "three.txt": "",
1426 "a": {
1427 "0": { "q": "", "r": "", "s": "" },
1428 "1": { "t": "", "u": "" },
1429 "2": { "v": "", "w": "", "x": "", "y": "" },
1430 },
1431 }),
1432 )
1433 .await;
1434
1435 fs.insert_tree(
1436 "/root2",
1437 json!({
1438 "one.txt": "",
1439 "two.txt": "",
1440 "four.txt": "",
1441 "b": {
1442 "3": { "Q": "" },
1443 "4": { "R": "", "S": "", "T": "", "U": "" },
1444 },
1445 }),
1446 )
1447 .await;
1448
1449 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1450 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1451 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1452 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1453
1454 select_path(&panel, "root1/three.txt", cx);
1455 panel.update_in(cx, |panel, window, cx| {
1456 panel.copy(&Default::default(), window, cx);
1457 });
1458
1459 select_path(&panel, "root2/one.txt", cx);
1460 panel.update_in(cx, |panel, window, cx| {
1461 panel.select_next(&Default::default(), window, cx);
1462 panel.paste(&Default::default(), window, cx);
1463 });
1464 cx.executor().run_until_parked();
1465 assert_eq!(
1466 visible_entries_as_strings(&panel, 0..50, cx),
1467 &[
1468 //
1469 "v root1",
1470 " > a",
1471 " one.txt",
1472 " three.txt",
1473 " two.txt",
1474 "v root2",
1475 " > b",
1476 " four.txt",
1477 " one.txt",
1478 " three.txt <== selected <== marked",
1479 " two.txt",
1480 ]
1481 );
1482
1483 select_path(&panel, "root1/three.txt", cx);
1484 panel.update_in(cx, |panel, window, cx| {
1485 panel.copy(&Default::default(), window, cx);
1486 });
1487 select_path(&panel, "root2/two.txt", cx);
1488 panel.update_in(cx, |panel, window, cx| {
1489 panel.select_next(&Default::default(), window, cx);
1490 panel.paste(&Default::default(), window, cx);
1491 });
1492
1493 cx.executor().run_until_parked();
1494 assert_eq!(
1495 visible_entries_as_strings(&panel, 0..50, cx),
1496 &[
1497 //
1498 "v root1",
1499 " > a",
1500 " one.txt",
1501 " three.txt",
1502 " two.txt",
1503 "v root2",
1504 " > b",
1505 " four.txt",
1506 " one.txt",
1507 " three.txt",
1508 " [EDITOR: 'three copy.txt'] <== selected <== marked",
1509 " two.txt",
1510 ]
1511 );
1512
1513 panel.update_in(cx, |panel, window, cx| {
1514 panel.cancel(&menu::Cancel {}, window, cx)
1515 });
1516 cx.executor().run_until_parked();
1517
1518 select_path(&panel, "root1/a", cx);
1519 panel.update_in(cx, |panel, window, cx| {
1520 panel.copy(&Default::default(), window, cx);
1521 });
1522 select_path(&panel, "root2/two.txt", cx);
1523 panel.update_in(cx, |panel, window, cx| {
1524 panel.select_next(&Default::default(), window, cx);
1525 panel.paste(&Default::default(), window, cx);
1526 });
1527
1528 cx.executor().run_until_parked();
1529 assert_eq!(
1530 visible_entries_as_strings(&panel, 0..50, cx),
1531 &[
1532 //
1533 "v root1",
1534 " > a",
1535 " one.txt",
1536 " three.txt",
1537 " two.txt",
1538 "v root2",
1539 " > a <== selected",
1540 " > b",
1541 " four.txt",
1542 " one.txt",
1543 " three.txt",
1544 " three copy.txt",
1545 " two.txt",
1546 ]
1547 );
1548}
1549
1550#[gpui::test]
1551async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) {
1552 init_test(cx);
1553
1554 let fs = FakeFs::new(cx.executor());
1555 fs.insert_tree(
1556 "/root",
1557 json!({
1558 "a": {
1559 "one.txt": "",
1560 "two.txt": "",
1561 "inner_dir": {
1562 "three.txt": "",
1563 "four.txt": "",
1564 }
1565 },
1566 "b": {}
1567 }),
1568 )
1569 .await;
1570
1571 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
1572 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1573 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1574 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1575
1576 select_path(&panel, "root/a", cx);
1577 panel.update_in(cx, |panel, window, cx| {
1578 panel.copy(&Default::default(), window, cx);
1579 panel.select_next(&Default::default(), window, cx);
1580 panel.paste(&Default::default(), window, cx);
1581 });
1582 cx.executor().run_until_parked();
1583
1584 let pasted_dir = find_project_entry(&panel, "root/b/a", cx);
1585 assert_ne!(pasted_dir, None, "Pasted directory should have an entry");
1586
1587 let pasted_dir_file = find_project_entry(&panel, "root/b/a/one.txt", cx);
1588 assert_ne!(
1589 pasted_dir_file, None,
1590 "Pasted directory file should have an entry"
1591 );
1592
1593 let pasted_dir_inner_dir = find_project_entry(&panel, "root/b/a/inner_dir", cx);
1594 assert_ne!(
1595 pasted_dir_inner_dir, None,
1596 "Directories inside pasted directory should have an entry"
1597 );
1598
1599 toggle_expand_dir(&panel, "root/b/a", cx);
1600 toggle_expand_dir(&panel, "root/b/a/inner_dir", cx);
1601
1602 assert_eq!(
1603 visible_entries_as_strings(&panel, 0..50, cx),
1604 &[
1605 //
1606 "v root",
1607 " > a",
1608 " v b",
1609 " v a",
1610 " v inner_dir <== selected",
1611 " four.txt",
1612 " three.txt",
1613 " one.txt",
1614 " two.txt",
1615 ]
1616 );
1617
1618 select_path(&panel, "root", cx);
1619 panel.update_in(cx, |panel, window, cx| {
1620 panel.paste(&Default::default(), window, cx)
1621 });
1622 cx.executor().run_until_parked();
1623 assert_eq!(
1624 visible_entries_as_strings(&panel, 0..50, cx),
1625 &[
1626 //
1627 "v root",
1628 " > a",
1629 " > [EDITOR: 'a copy'] <== selected",
1630 " v b",
1631 " v a",
1632 " v inner_dir",
1633 " four.txt",
1634 " three.txt",
1635 " one.txt",
1636 " two.txt"
1637 ]
1638 );
1639
1640 let confirm = panel.update_in(cx, |panel, window, cx| {
1641 panel
1642 .filename_editor
1643 .update(cx, |editor, cx| editor.set_text("c", window, cx));
1644 panel.confirm_edit(window, cx).unwrap()
1645 });
1646 assert_eq!(
1647 visible_entries_as_strings(&panel, 0..50, cx),
1648 &[
1649 //
1650 "v root",
1651 " > a",
1652 " > [PROCESSING: 'c'] <== selected",
1653 " v b",
1654 " v a",
1655 " v inner_dir",
1656 " four.txt",
1657 " three.txt",
1658 " one.txt",
1659 " two.txt"
1660 ]
1661 );
1662
1663 confirm.await.unwrap();
1664
1665 panel.update_in(cx, |panel, window, cx| {
1666 panel.paste(&Default::default(), window, cx)
1667 });
1668 cx.executor().run_until_parked();
1669 assert_eq!(
1670 visible_entries_as_strings(&panel, 0..50, cx),
1671 &[
1672 //
1673 "v root",
1674 " > a",
1675 " v b",
1676 " v a",
1677 " v inner_dir",
1678 " four.txt",
1679 " three.txt",
1680 " one.txt",
1681 " two.txt",
1682 " v c",
1683 " > a <== selected",
1684 " > inner_dir",
1685 " one.txt",
1686 " two.txt",
1687 ]
1688 );
1689}
1690
1691#[gpui::test]
1692async fn test_copy_paste_directory_with_sibling_file(cx: &mut gpui::TestAppContext) {
1693 init_test(cx);
1694
1695 let fs = FakeFs::new(cx.executor());
1696 fs.insert_tree(
1697 "/test",
1698 json!({
1699 "dir1": {
1700 "a.txt": "",
1701 "b.txt": "",
1702 },
1703 "dir2": {},
1704 "c.txt": "",
1705 "d.txt": "",
1706 }),
1707 )
1708 .await;
1709
1710 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1711 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1712 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1713 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1714
1715 toggle_expand_dir(&panel, "test/dir1", cx);
1716
1717 cx.simulate_modifiers_change(gpui::Modifiers {
1718 control: true,
1719 ..Default::default()
1720 });
1721
1722 select_path_with_mark(&panel, "test/dir1", cx);
1723 select_path_with_mark(&panel, "test/c.txt", cx);
1724
1725 assert_eq!(
1726 visible_entries_as_strings(&panel, 0..15, cx),
1727 &[
1728 "v test",
1729 " v dir1 <== marked",
1730 " a.txt",
1731 " b.txt",
1732 " > dir2",
1733 " c.txt <== selected <== marked",
1734 " d.txt",
1735 ],
1736 "Initial state before copying dir1 and c.txt"
1737 );
1738
1739 panel.update_in(cx, |panel, window, cx| {
1740 panel.copy(&Default::default(), window, cx);
1741 });
1742 select_path(&panel, "test/dir2", cx);
1743 panel.update_in(cx, |panel, window, cx| {
1744 panel.paste(&Default::default(), window, cx);
1745 });
1746 cx.executor().run_until_parked();
1747
1748 toggle_expand_dir(&panel, "test/dir2/dir1", cx);
1749
1750 assert_eq!(
1751 visible_entries_as_strings(&panel, 0..15, cx),
1752 &[
1753 "v test",
1754 " v dir1 <== marked",
1755 " a.txt",
1756 " b.txt",
1757 " v dir2",
1758 " v dir1 <== selected",
1759 " a.txt",
1760 " b.txt",
1761 " c.txt",
1762 " c.txt <== marked",
1763 " d.txt",
1764 ],
1765 "Should copy dir1 as well as c.txt into dir2"
1766 );
1767
1768 // Disambiguating multiple files should not open the rename editor.
1769 select_path(&panel, "test/dir2", cx);
1770 panel.update_in(cx, |panel, window, cx| {
1771 panel.paste(&Default::default(), window, cx);
1772 });
1773 cx.executor().run_until_parked();
1774
1775 assert_eq!(
1776 visible_entries_as_strings(&panel, 0..15, cx),
1777 &[
1778 "v test",
1779 " v dir1 <== marked",
1780 " a.txt",
1781 " b.txt",
1782 " v dir2",
1783 " v dir1",
1784 " a.txt",
1785 " b.txt",
1786 " > dir1 copy <== selected",
1787 " c.txt",
1788 " c copy.txt",
1789 " c.txt <== marked",
1790 " d.txt",
1791 ],
1792 "Should copy dir1 as well as c.txt into dir2 and disambiguate them without opening the rename editor"
1793 );
1794}
1795
1796#[gpui::test]
1797async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) {
1798 init_test(cx);
1799
1800 let fs = FakeFs::new(cx.executor());
1801 fs.insert_tree(
1802 "/test",
1803 json!({
1804 "dir1": {
1805 "a.txt": "",
1806 "b.txt": "",
1807 },
1808 "dir2": {},
1809 "c.txt": "",
1810 "d.txt": "",
1811 }),
1812 )
1813 .await;
1814
1815 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1816 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1817 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1818 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1819
1820 toggle_expand_dir(&panel, "test/dir1", cx);
1821
1822 cx.simulate_modifiers_change(gpui::Modifiers {
1823 control: true,
1824 ..Default::default()
1825 });
1826
1827 select_path_with_mark(&panel, "test/dir1/a.txt", cx);
1828 select_path_with_mark(&panel, "test/dir1", cx);
1829 select_path_with_mark(&panel, "test/c.txt", cx);
1830
1831 assert_eq!(
1832 visible_entries_as_strings(&panel, 0..15, cx),
1833 &[
1834 "v test",
1835 " v dir1 <== marked",
1836 " a.txt <== marked",
1837 " b.txt",
1838 " > dir2",
1839 " c.txt <== selected <== marked",
1840 " d.txt",
1841 ],
1842 "Initial state before copying a.txt, dir1 and c.txt"
1843 );
1844
1845 panel.update_in(cx, |panel, window, cx| {
1846 panel.copy(&Default::default(), window, cx);
1847 });
1848 select_path(&panel, "test/dir2", cx);
1849 panel.update_in(cx, |panel, window, cx| {
1850 panel.paste(&Default::default(), window, cx);
1851 });
1852 cx.executor().run_until_parked();
1853
1854 toggle_expand_dir(&panel, "test/dir2/dir1", cx);
1855
1856 assert_eq!(
1857 visible_entries_as_strings(&panel, 0..20, cx),
1858 &[
1859 "v test",
1860 " v dir1 <== marked",
1861 " a.txt <== marked",
1862 " b.txt",
1863 " v dir2",
1864 " v dir1 <== selected",
1865 " a.txt",
1866 " b.txt",
1867 " c.txt",
1868 " c.txt <== marked",
1869 " d.txt",
1870 ],
1871 "Should copy dir1 and c.txt into dir2. a.txt is already present in copied dir1."
1872 );
1873}
1874
1875#[gpui::test]
1876async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
1877 init_test_with_editor(cx);
1878
1879 let fs = FakeFs::new(cx.executor());
1880 fs.insert_tree(
1881 path!("/src"),
1882 json!({
1883 "test": {
1884 "first.rs": "// First Rust file",
1885 "second.rs": "// Second Rust file",
1886 "third.rs": "// Third Rust file",
1887 }
1888 }),
1889 )
1890 .await;
1891
1892 let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await;
1893 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1894 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1895 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1896
1897 toggle_expand_dir(&panel, "src/test", cx);
1898 select_path(&panel, "src/test/first.rs", cx);
1899 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
1900 cx.executor().run_until_parked();
1901 assert_eq!(
1902 visible_entries_as_strings(&panel, 0..10, cx),
1903 &[
1904 "v src",
1905 " v test",
1906 " first.rs <== selected <== marked",
1907 " second.rs",
1908 " third.rs"
1909 ]
1910 );
1911 ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
1912
1913 submit_deletion(&panel, cx);
1914 assert_eq!(
1915 visible_entries_as_strings(&panel, 0..10, cx),
1916 &[
1917 "v src",
1918 " v test",
1919 " second.rs <== selected",
1920 " third.rs"
1921 ],
1922 "Project panel should have no deleted file, no other file is selected in it"
1923 );
1924 ensure_no_open_items_and_panes(&workspace, cx);
1925
1926 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
1927 cx.executor().run_until_parked();
1928 assert_eq!(
1929 visible_entries_as_strings(&panel, 0..10, cx),
1930 &[
1931 "v src",
1932 " v test",
1933 " second.rs <== selected <== marked",
1934 " third.rs"
1935 ]
1936 );
1937 ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
1938
1939 workspace
1940 .update(cx, |workspace, window, cx| {
1941 let active_items = workspace
1942 .panes()
1943 .iter()
1944 .filter_map(|pane| pane.read(cx).active_item())
1945 .collect::<Vec<_>>();
1946 assert_eq!(active_items.len(), 1);
1947 let open_editor = active_items
1948 .into_iter()
1949 .next()
1950 .unwrap()
1951 .downcast::<Editor>()
1952 .expect("Open item should be an editor");
1953 open_editor.update(cx, |editor, cx| {
1954 editor.set_text("Another text!", window, cx)
1955 });
1956 })
1957 .unwrap();
1958 submit_deletion_skipping_prompt(&panel, cx);
1959 assert_eq!(
1960 visible_entries_as_strings(&panel, 0..10, cx),
1961 &["v src", " v test", " third.rs <== selected"],
1962 "Project panel should have no deleted file, with one last file remaining"
1963 );
1964 ensure_no_open_items_and_panes(&workspace, cx);
1965}
1966
1967#[gpui::test]
1968async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
1969 init_test_with_editor(cx);
1970
1971 let fs = FakeFs::new(cx.executor());
1972 fs.insert_tree(
1973 "/src",
1974 json!({
1975 "test": {
1976 "first.rs": "// First Rust file",
1977 "second.rs": "// Second Rust file",
1978 "third.rs": "// Third Rust file",
1979 }
1980 }),
1981 )
1982 .await;
1983
1984 let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
1985 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1986 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1987 let panel = workspace
1988 .update(cx, |workspace, window, cx| {
1989 let panel = ProjectPanel::new(workspace, window, cx);
1990 workspace.add_panel(panel.clone(), window, cx);
1991 panel
1992 })
1993 .unwrap();
1994
1995 select_path(&panel, "src/", cx);
1996 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
1997 cx.executor().run_until_parked();
1998 assert_eq!(
1999 visible_entries_as_strings(&panel, 0..10, cx),
2000 &[
2001 //
2002 "v src <== selected",
2003 " > test"
2004 ]
2005 );
2006 panel.update_in(cx, |panel, window, cx| {
2007 panel.new_directory(&NewDirectory, window, cx)
2008 });
2009 panel.update_in(cx, |panel, window, cx| {
2010 assert!(panel.filename_editor.read(cx).is_focused(window));
2011 });
2012 assert_eq!(
2013 visible_entries_as_strings(&panel, 0..10, cx),
2014 &[
2015 //
2016 "v src",
2017 " > [EDITOR: ''] <== selected",
2018 " > test"
2019 ]
2020 );
2021 panel.update_in(cx, |panel, window, cx| {
2022 panel
2023 .filename_editor
2024 .update(cx, |editor, cx| editor.set_text("test", window, cx));
2025 assert!(
2026 panel.confirm_edit(window, cx).is_none(),
2027 "Should not allow to confirm on conflicting new directory name"
2028 );
2029 });
2030 cx.executor().run_until_parked();
2031 panel.update_in(cx, |panel, window, cx| {
2032 assert!(
2033 panel.edit_state.is_some(),
2034 "Edit state should not be None after conflicting new directory name"
2035 );
2036 panel.cancel(&menu::Cancel, window, cx);
2037 });
2038 assert_eq!(
2039 visible_entries_as_strings(&panel, 0..10, cx),
2040 &[
2041 //
2042 "v src <== selected",
2043 " > test"
2044 ],
2045 "File list should be unchanged after failed folder create confirmation"
2046 );
2047
2048 select_path(&panel, "src/test/", cx);
2049 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2050 cx.executor().run_until_parked();
2051 assert_eq!(
2052 visible_entries_as_strings(&panel, 0..10, cx),
2053 &[
2054 //
2055 "v src",
2056 " > test <== selected"
2057 ]
2058 );
2059 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2060 panel.update_in(cx, |panel, window, cx| {
2061 assert!(panel.filename_editor.read(cx).is_focused(window));
2062 });
2063 assert_eq!(
2064 visible_entries_as_strings(&panel, 0..10, cx),
2065 &[
2066 "v src",
2067 " v test",
2068 " [EDITOR: ''] <== selected",
2069 " first.rs",
2070 " second.rs",
2071 " third.rs"
2072 ]
2073 );
2074 panel.update_in(cx, |panel, window, cx| {
2075 panel
2076 .filename_editor
2077 .update(cx, |editor, cx| editor.set_text("first.rs", window, cx));
2078 assert!(
2079 panel.confirm_edit(window, cx).is_none(),
2080 "Should not allow to confirm on conflicting new file name"
2081 );
2082 });
2083 cx.executor().run_until_parked();
2084 panel.update_in(cx, |panel, window, cx| {
2085 assert!(
2086 panel.edit_state.is_some(),
2087 "Edit state should not be None after conflicting new file name"
2088 );
2089 panel.cancel(&menu::Cancel, window, cx);
2090 });
2091 assert_eq!(
2092 visible_entries_as_strings(&panel, 0..10, cx),
2093 &[
2094 "v src",
2095 " v test <== selected",
2096 " first.rs",
2097 " second.rs",
2098 " third.rs"
2099 ],
2100 "File list should be unchanged after failed file create confirmation"
2101 );
2102
2103 select_path(&panel, "src/test/first.rs", cx);
2104 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2105 cx.executor().run_until_parked();
2106 assert_eq!(
2107 visible_entries_as_strings(&panel, 0..10, cx),
2108 &[
2109 "v src",
2110 " v test",
2111 " first.rs <== selected",
2112 " second.rs",
2113 " third.rs"
2114 ],
2115 );
2116 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2117 panel.update_in(cx, |panel, window, cx| {
2118 assert!(panel.filename_editor.read(cx).is_focused(window));
2119 });
2120 assert_eq!(
2121 visible_entries_as_strings(&panel, 0..10, cx),
2122 &[
2123 "v src",
2124 " v test",
2125 " [EDITOR: 'first.rs'] <== selected",
2126 " second.rs",
2127 " third.rs"
2128 ]
2129 );
2130 panel.update_in(cx, |panel, window, cx| {
2131 panel
2132 .filename_editor
2133 .update(cx, |editor, cx| editor.set_text("second.rs", window, cx));
2134 assert!(
2135 panel.confirm_edit(window, cx).is_none(),
2136 "Should not allow to confirm on conflicting file rename"
2137 )
2138 });
2139 cx.executor().run_until_parked();
2140 panel.update_in(cx, |panel, window, cx| {
2141 assert!(
2142 panel.edit_state.is_some(),
2143 "Edit state should not be None after conflicting file rename"
2144 );
2145 panel.cancel(&menu::Cancel, window, cx);
2146 });
2147 assert_eq!(
2148 visible_entries_as_strings(&panel, 0..10, cx),
2149 &[
2150 "v src",
2151 " v test",
2152 " first.rs <== selected",
2153 " second.rs",
2154 " third.rs"
2155 ],
2156 "File list should be unchanged after failed rename confirmation"
2157 );
2158}
2159
2160#[gpui::test]
2161async fn test_select_git_entry(cx: &mut gpui::TestAppContext) {
2162 init_test_with_editor(cx);
2163
2164 let fs = FakeFs::new(cx.executor());
2165 fs.insert_tree(
2166 path!("/root"),
2167 json!({
2168 "tree1": {
2169 ".git": {},
2170 "dir1": {
2171 "modified1.txt": "1",
2172 "unmodified1.txt": "1",
2173 "modified2.txt": "1",
2174 },
2175 "dir2": {
2176 "modified3.txt": "1",
2177 "unmodified2.txt": "1",
2178 },
2179 "modified4.txt": "1",
2180 "unmodified3.txt": "1",
2181 },
2182 "tree2": {
2183 ".git": {},
2184 "dir3": {
2185 "modified5.txt": "1",
2186 "unmodified4.txt": "1",
2187 },
2188 "modified6.txt": "1",
2189 "unmodified5.txt": "1",
2190 }
2191 }),
2192 )
2193 .await;
2194
2195 // Mark files as git modified
2196 fs.set_git_content_for_repo(
2197 path!("/root/tree1/.git").as_ref(),
2198 &[
2199 ("dir1/modified1.txt".into(), "modified".into(), None),
2200 ("dir1/modified2.txt".into(), "modified".into(), None),
2201 ("modified4.txt".into(), "modified".into(), None),
2202 ("dir2/modified3.txt".into(), "modified".into(), None),
2203 ],
2204 );
2205 fs.set_git_content_for_repo(
2206 path!("/root/tree2/.git").as_ref(),
2207 &[
2208 ("dir3/modified5.txt".into(), "modified".into(), None),
2209 ("modified6.txt".into(), "modified".into(), None),
2210 ],
2211 );
2212
2213 let project = Project::test(
2214 fs.clone(),
2215 [path!("/root/tree1").as_ref(), path!("/root/tree2").as_ref()],
2216 cx,
2217 )
2218 .await;
2219
2220 let (scan1_complete, scan2_complete) = project.update(cx, |project, cx| {
2221 let mut worktrees = project.worktrees(cx);
2222 let worktree1 = worktrees.next().unwrap();
2223 let worktree2 = worktrees.next().unwrap();
2224 (
2225 worktree1.read(cx).as_local().unwrap().scan_complete(),
2226 worktree2.read(cx).as_local().unwrap().scan_complete(),
2227 )
2228 });
2229 scan1_complete.await;
2230 scan2_complete.await;
2231 cx.run_until_parked();
2232
2233 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2234 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2235 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2236
2237 // Check initial state
2238 assert_eq!(
2239 visible_entries_as_strings(&panel, 0..15, cx),
2240 &[
2241 "v tree1",
2242 " > .git",
2243 " > dir1",
2244 " > dir2",
2245 " modified4.txt",
2246 " unmodified3.txt",
2247 "v tree2",
2248 " > .git",
2249 " > dir3",
2250 " modified6.txt",
2251 " unmodified5.txt"
2252 ],
2253 );
2254
2255 // Test selecting next modified entry
2256 panel.update_in(cx, |panel, window, cx| {
2257 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2258 });
2259
2260 assert_eq!(
2261 visible_entries_as_strings(&panel, 0..6, cx),
2262 &[
2263 "v tree1",
2264 " > .git",
2265 " v dir1",
2266 " modified1.txt <== selected",
2267 " modified2.txt",
2268 " unmodified1.txt",
2269 ],
2270 );
2271
2272 panel.update_in(cx, |panel, window, cx| {
2273 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2274 });
2275
2276 assert_eq!(
2277 visible_entries_as_strings(&panel, 0..6, cx),
2278 &[
2279 "v tree1",
2280 " > .git",
2281 " v dir1",
2282 " modified1.txt",
2283 " modified2.txt <== selected",
2284 " unmodified1.txt",
2285 ],
2286 );
2287
2288 panel.update_in(cx, |panel, window, cx| {
2289 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2290 });
2291
2292 assert_eq!(
2293 visible_entries_as_strings(&panel, 6..9, cx),
2294 &[
2295 " v dir2",
2296 " modified3.txt <== selected",
2297 " unmodified2.txt",
2298 ],
2299 );
2300
2301 panel.update_in(cx, |panel, window, cx| {
2302 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2303 });
2304
2305 assert_eq!(
2306 visible_entries_as_strings(&panel, 9..11, cx),
2307 &[" modified4.txt <== selected", " unmodified3.txt",],
2308 );
2309
2310 panel.update_in(cx, |panel, window, cx| {
2311 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2312 });
2313
2314 assert_eq!(
2315 visible_entries_as_strings(&panel, 13..16, cx),
2316 &[
2317 " v dir3",
2318 " modified5.txt <== selected",
2319 " unmodified4.txt",
2320 ],
2321 );
2322
2323 panel.update_in(cx, |panel, window, cx| {
2324 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2325 });
2326
2327 assert_eq!(
2328 visible_entries_as_strings(&panel, 16..18, cx),
2329 &[" modified6.txt <== selected", " unmodified5.txt",],
2330 );
2331
2332 // Wraps around to first modified file
2333 panel.update_in(cx, |panel, window, cx| {
2334 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2335 });
2336
2337 assert_eq!(
2338 visible_entries_as_strings(&panel, 0..18, cx),
2339 &[
2340 "v tree1",
2341 " > .git",
2342 " v dir1",
2343 " modified1.txt <== selected",
2344 " modified2.txt",
2345 " unmodified1.txt",
2346 " v dir2",
2347 " modified3.txt",
2348 " unmodified2.txt",
2349 " modified4.txt",
2350 " unmodified3.txt",
2351 "v tree2",
2352 " > .git",
2353 " v dir3",
2354 " modified5.txt",
2355 " unmodified4.txt",
2356 " modified6.txt",
2357 " unmodified5.txt",
2358 ],
2359 );
2360
2361 // Wraps around again to last modified file
2362 panel.update_in(cx, |panel, window, cx| {
2363 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2364 });
2365
2366 assert_eq!(
2367 visible_entries_as_strings(&panel, 16..18, cx),
2368 &[" modified6.txt <== selected", " unmodified5.txt",],
2369 );
2370
2371 panel.update_in(cx, |panel, window, cx| {
2372 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2373 });
2374
2375 assert_eq!(
2376 visible_entries_as_strings(&panel, 13..16, cx),
2377 &[
2378 " v dir3",
2379 " modified5.txt <== selected",
2380 " unmodified4.txt",
2381 ],
2382 );
2383
2384 panel.update_in(cx, |panel, window, cx| {
2385 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2386 });
2387
2388 assert_eq!(
2389 visible_entries_as_strings(&panel, 9..11, cx),
2390 &[" modified4.txt <== selected", " unmodified3.txt",],
2391 );
2392
2393 panel.update_in(cx, |panel, window, cx| {
2394 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2395 });
2396
2397 assert_eq!(
2398 visible_entries_as_strings(&panel, 6..9, cx),
2399 &[
2400 " v dir2",
2401 " modified3.txt <== selected",
2402 " unmodified2.txt",
2403 ],
2404 );
2405
2406 panel.update_in(cx, |panel, window, cx| {
2407 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2408 });
2409
2410 assert_eq!(
2411 visible_entries_as_strings(&panel, 0..6, cx),
2412 &[
2413 "v tree1",
2414 " > .git",
2415 " v dir1",
2416 " modified1.txt",
2417 " modified2.txt <== selected",
2418 " unmodified1.txt",
2419 ],
2420 );
2421
2422 panel.update_in(cx, |panel, window, cx| {
2423 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
2424 });
2425
2426 assert_eq!(
2427 visible_entries_as_strings(&panel, 0..6, cx),
2428 &[
2429 "v tree1",
2430 " > .git",
2431 " v dir1",
2432 " modified1.txt <== selected",
2433 " modified2.txt",
2434 " unmodified1.txt",
2435 ],
2436 );
2437}
2438
2439#[gpui::test]
2440async fn test_select_directory(cx: &mut gpui::TestAppContext) {
2441 init_test_with_editor(cx);
2442
2443 let fs = FakeFs::new(cx.executor());
2444 fs.insert_tree(
2445 "/project_root",
2446 json!({
2447 "dir_1": {
2448 "nested_dir": {
2449 "file_a.py": "# File contents",
2450 }
2451 },
2452 "file_1.py": "# File contents",
2453 "dir_2": {
2454
2455 },
2456 "dir_3": {
2457
2458 },
2459 "file_2.py": "# File contents",
2460 "dir_4": {
2461
2462 },
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 cx.executor().run_until_parked();
2476 assert_eq!(
2477 visible_entries_as_strings(&panel, 0..10, cx),
2478 &[
2479 "v project_root",
2480 " > dir_1 <== selected",
2481 " > dir_2",
2482 " > dir_3",
2483 " > dir_4",
2484 " file_1.py",
2485 " file_2.py",
2486 ]
2487 );
2488 panel.update_in(cx, |panel, window, cx| {
2489 panel.select_prev_directory(&SelectPrevDirectory, window, cx)
2490 });
2491
2492 assert_eq!(
2493 visible_entries_as_strings(&panel, 0..10, cx),
2494 &[
2495 "v project_root <== selected",
2496 " > dir_1",
2497 " > dir_2",
2498 " > dir_3",
2499 " > dir_4",
2500 " file_1.py",
2501 " file_2.py",
2502 ]
2503 );
2504
2505 panel.update_in(cx, |panel, window, cx| {
2506 panel.select_prev_directory(&SelectPrevDirectory, window, cx)
2507 });
2508
2509 assert_eq!(
2510 visible_entries_as_strings(&panel, 0..10, cx),
2511 &[
2512 "v project_root",
2513 " > dir_1",
2514 " > dir_2",
2515 " > dir_3",
2516 " > dir_4 <== selected",
2517 " file_1.py",
2518 " file_2.py",
2519 ]
2520 );
2521
2522 panel.update_in(cx, |panel, window, cx| {
2523 panel.select_next_directory(&SelectNextDirectory, window, cx)
2524 });
2525
2526 assert_eq!(
2527 visible_entries_as_strings(&panel, 0..10, cx),
2528 &[
2529 "v project_root <== selected",
2530 " > dir_1",
2531 " > dir_2",
2532 " > dir_3",
2533 " > dir_4",
2534 " file_1.py",
2535 " file_2.py",
2536 ]
2537 );
2538}
2539
2540#[gpui::test]
2541async fn test_select_first_last(cx: &mut gpui::TestAppContext) {
2542 init_test_with_editor(cx);
2543
2544 let fs = FakeFs::new(cx.executor());
2545 fs.insert_tree(
2546 "/project_root",
2547 json!({
2548 "dir_1": {
2549 "nested_dir": {
2550 "file_a.py": "# File contents",
2551 }
2552 },
2553 "file_1.py": "# File contents",
2554 "file_2.py": "# File contents",
2555 "zdir_2": {
2556 "nested_dir2": {
2557 "file_b.py": "# File contents",
2558 }
2559 },
2560 }),
2561 )
2562 .await;
2563
2564 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2565 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2566 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2567 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2568
2569 assert_eq!(
2570 visible_entries_as_strings(&panel, 0..10, cx),
2571 &[
2572 "v project_root",
2573 " > dir_1",
2574 " > zdir_2",
2575 " file_1.py",
2576 " file_2.py",
2577 ]
2578 );
2579 panel.update_in(cx, |panel, window, cx| {
2580 panel.select_first(&SelectFirst, window, cx)
2581 });
2582
2583 assert_eq!(
2584 visible_entries_as_strings(&panel, 0..10, cx),
2585 &[
2586 "v project_root <== selected",
2587 " > dir_1",
2588 " > zdir_2",
2589 " file_1.py",
2590 " file_2.py",
2591 ]
2592 );
2593
2594 panel.update_in(cx, |panel, window, cx| {
2595 panel.select_last(&SelectLast, window, cx)
2596 });
2597
2598 assert_eq!(
2599 visible_entries_as_strings(&panel, 0..10, cx),
2600 &[
2601 "v project_root",
2602 " > dir_1",
2603 " > zdir_2",
2604 " file_1.py",
2605 " file_2.py <== selected",
2606 ]
2607 );
2608
2609 cx.update(|_, cx| {
2610 let settings = *ProjectPanelSettings::get_global(cx);
2611 ProjectPanelSettings::override_global(
2612 ProjectPanelSettings {
2613 hide_root: true,
2614 ..settings
2615 },
2616 cx,
2617 );
2618 });
2619
2620 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2621
2622 #[rustfmt::skip]
2623 assert_eq!(
2624 visible_entries_as_strings(&panel, 0..10, cx),
2625 &[
2626 "> dir_1",
2627 "> zdir_2",
2628 " file_1.py",
2629 " file_2.py",
2630 ],
2631 "With hide_root=true, root should be hidden"
2632 );
2633
2634 panel.update_in(cx, |panel, window, cx| {
2635 panel.select_first(&SelectFirst, window, cx)
2636 });
2637
2638 assert_eq!(
2639 visible_entries_as_strings(&panel, 0..10, cx),
2640 &[
2641 "> dir_1 <== selected",
2642 "> zdir_2",
2643 " file_1.py",
2644 " file_2.py",
2645 ],
2646 "With hide_root=true, first entry should be dir_1, not the hidden root"
2647 );
2648}
2649
2650#[gpui::test]
2651async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) {
2652 init_test_with_editor(cx);
2653
2654 let fs = FakeFs::new(cx.executor());
2655 fs.insert_tree(
2656 "/project_root",
2657 json!({
2658 "dir_1": {
2659 "nested_dir": {
2660 "file_a.py": "# File contents",
2661 }
2662 },
2663 "file_1.py": "# File contents",
2664 }),
2665 )
2666 .await;
2667
2668 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2669 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2670 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2671 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2672
2673 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2674 cx.executor().run_until_parked();
2675 select_path(&panel, "project_root/dir_1", cx);
2676 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2677 select_path(&panel, "project_root/dir_1/nested_dir", cx);
2678 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2679 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2680 cx.executor().run_until_parked();
2681 assert_eq!(
2682 visible_entries_as_strings(&panel, 0..10, cx),
2683 &[
2684 "v project_root",
2685 " v dir_1",
2686 " > nested_dir <== selected",
2687 " file_1.py",
2688 ]
2689 );
2690}
2691
2692#[gpui::test]
2693async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
2694 init_test_with_editor(cx);
2695
2696 let fs = FakeFs::new(cx.executor());
2697 fs.insert_tree(
2698 "/project_root",
2699 json!({
2700 "dir_1": {
2701 "nested_dir": {
2702 "file_a.py": "# File contents",
2703 "file_b.py": "# File contents",
2704 "file_c.py": "# File contents",
2705 },
2706 "file_1.py": "# File contents",
2707 "file_2.py": "# File contents",
2708 "file_3.py": "# File contents",
2709 },
2710 "dir_2": {
2711 "file_1.py": "# File contents",
2712 "file_2.py": "# File contents",
2713 "file_3.py": "# File contents",
2714 }
2715 }),
2716 )
2717 .await;
2718
2719 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2720 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2721 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2722 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2723
2724 panel.update_in(cx, |panel, window, cx| {
2725 panel.collapse_all_entries(&CollapseAllEntries, window, cx)
2726 });
2727 cx.executor().run_until_parked();
2728 assert_eq!(
2729 visible_entries_as_strings(&panel, 0..10, cx),
2730 &["v project_root", " > dir_1", " > dir_2",]
2731 );
2732
2733 // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
2734 toggle_expand_dir(&panel, "project_root/dir_1", cx);
2735 cx.executor().run_until_parked();
2736 assert_eq!(
2737 visible_entries_as_strings(&panel, 0..10, cx),
2738 &[
2739 "v project_root",
2740 " v dir_1 <== selected",
2741 " > nested_dir",
2742 " file_1.py",
2743 " file_2.py",
2744 " file_3.py",
2745 " > dir_2",
2746 ]
2747 );
2748}
2749
2750#[gpui::test]
2751async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
2752 init_test(cx);
2753
2754 let fs = FakeFs::new(cx.executor());
2755 fs.as_fake().insert_tree(path!("/root"), json!({})).await;
2756 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
2757 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2758 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2759 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2760
2761 // Make a new buffer with no backing file
2762 workspace
2763 .update(cx, |workspace, window, cx| {
2764 Editor::new_file(workspace, &Default::default(), window, cx)
2765 })
2766 .unwrap();
2767
2768 cx.executor().run_until_parked();
2769
2770 // "Save as" the buffer, creating a new backing file for it
2771 let save_task = workspace
2772 .update(cx, |workspace, window, cx| {
2773 workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
2774 })
2775 .unwrap();
2776
2777 cx.executor().run_until_parked();
2778 cx.simulate_new_path_selection(|_| Some(PathBuf::from(path!("/root/new"))));
2779 save_task.await.unwrap();
2780
2781 // Rename the file
2782 select_path(&panel, "root/new", cx);
2783 assert_eq!(
2784 visible_entries_as_strings(&panel, 0..10, cx),
2785 &["v root", " new <== selected <== marked"]
2786 );
2787 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2788 panel.update_in(cx, |panel, window, cx| {
2789 panel
2790 .filename_editor
2791 .update(cx, |editor, cx| editor.set_text("newer", window, cx));
2792 });
2793 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2794
2795 cx.executor().run_until_parked();
2796 assert_eq!(
2797 visible_entries_as_strings(&panel, 0..10, cx),
2798 &["v root", " newer <== selected"]
2799 );
2800
2801 workspace
2802 .update(cx, |workspace, window, cx| {
2803 workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
2804 })
2805 .unwrap()
2806 .await
2807 .unwrap();
2808
2809 cx.executor().run_until_parked();
2810 // assert that saving the file doesn't restore "new"
2811 assert_eq!(
2812 visible_entries_as_strings(&panel, 0..10, cx),
2813 &["v root", " newer <== selected"]
2814 );
2815}
2816
2817#[gpui::test]
2818#[cfg_attr(target_os = "windows", ignore)]
2819async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
2820 init_test_with_editor(cx);
2821
2822 let fs = FakeFs::new(cx.executor());
2823 fs.insert_tree(
2824 "/root1",
2825 json!({
2826 "dir1": {
2827 "file1.txt": "content 1",
2828 },
2829 }),
2830 )
2831 .await;
2832
2833 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
2834 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2835 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2836 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2837
2838 toggle_expand_dir(&panel, "root1/dir1", cx);
2839
2840 assert_eq!(
2841 visible_entries_as_strings(&panel, 0..20, cx),
2842 &["v root1", " v dir1 <== selected", " file1.txt",],
2843 "Initial state with worktrees"
2844 );
2845
2846 select_path(&panel, "root1", cx);
2847 assert_eq!(
2848 visible_entries_as_strings(&panel, 0..20, cx),
2849 &["v root1 <== selected", " v dir1", " file1.txt",],
2850 );
2851
2852 // Rename root1 to new_root1
2853 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2854
2855 assert_eq!(
2856 visible_entries_as_strings(&panel, 0..20, cx),
2857 &[
2858 "v [EDITOR: 'root1'] <== selected",
2859 " v dir1",
2860 " file1.txt",
2861 ],
2862 );
2863
2864 let confirm = panel.update_in(cx, |panel, window, cx| {
2865 panel
2866 .filename_editor
2867 .update(cx, |editor, cx| editor.set_text("new_root1", window, cx));
2868 panel.confirm_edit(window, cx).unwrap()
2869 });
2870 confirm.await.unwrap();
2871 assert_eq!(
2872 visible_entries_as_strings(&panel, 0..20, cx),
2873 &[
2874 "v new_root1 <== selected",
2875 " v dir1",
2876 " file1.txt",
2877 ],
2878 "Should update worktree name"
2879 );
2880
2881 // Ensure internal paths have been updated
2882 select_path(&panel, "new_root1/dir1/file1.txt", cx);
2883 assert_eq!(
2884 visible_entries_as_strings(&panel, 0..20, cx),
2885 &[
2886 "v new_root1",
2887 " v dir1",
2888 " file1.txt <== selected",
2889 ],
2890 "Files in renamed worktree are selectable"
2891 );
2892}
2893
2894#[gpui::test]
2895async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) {
2896 init_test_with_editor(cx);
2897
2898 let fs = FakeFs::new(cx.executor());
2899 fs.insert_tree(
2900 "/root1",
2901 json!({
2902 "dir1": { "file1.txt": "content" },
2903 "file2.txt": "content",
2904 }),
2905 )
2906 .await;
2907 fs.insert_tree("/root2", json!({ "file3.txt": "content" }))
2908 .await;
2909
2910 // Test 1: Single worktree, hide_root=true - rename should be blocked
2911 {
2912 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
2913 let workspace =
2914 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2915 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2916
2917 cx.update(|_, cx| {
2918 let settings = *ProjectPanelSettings::get_global(cx);
2919 ProjectPanelSettings::override_global(
2920 ProjectPanelSettings {
2921 hide_root: true,
2922 ..settings
2923 },
2924 cx,
2925 );
2926 });
2927
2928 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2929
2930 panel.update(cx, |panel, cx| {
2931 let project = panel.project.read(cx);
2932 let worktree = project.visible_worktrees(cx).next().unwrap();
2933 let root_entry = worktree.read(cx).root_entry().unwrap();
2934 panel.selection = Some(SelectedEntry {
2935 worktree_id: worktree.read(cx).id(),
2936 entry_id: root_entry.id,
2937 });
2938 });
2939
2940 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2941
2942 assert!(
2943 panel.read_with(cx, |panel, _| panel.edit_state.is_none()),
2944 "Rename should be blocked when hide_root=true with single worktree"
2945 );
2946 }
2947
2948 // Test 2: Multiple worktrees, hide_root=true - rename should work
2949 {
2950 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
2951 let workspace =
2952 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2953 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2954
2955 cx.update(|_, cx| {
2956 let settings = *ProjectPanelSettings::get_global(cx);
2957 ProjectPanelSettings::override_global(
2958 ProjectPanelSettings {
2959 hide_root: true,
2960 ..settings
2961 },
2962 cx,
2963 );
2964 });
2965
2966 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2967 select_path(&panel, "root1", cx);
2968 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2969
2970 #[cfg(target_os = "windows")]
2971 assert!(
2972 panel.read_with(cx, |panel, _| panel.edit_state.is_none()),
2973 "Rename should be blocked on Windows even with multiple worktrees"
2974 );
2975
2976 #[cfg(not(target_os = "windows"))]
2977 {
2978 assert!(
2979 panel.read_with(cx, |panel, _| panel.edit_state.is_some()),
2980 "Rename should work with multiple worktrees on non-Windows when hide_root=true"
2981 );
2982 panel.update_in(cx, |panel, window, cx| {
2983 panel.cancel(&menu::Cancel, window, cx)
2984 });
2985 }
2986 }
2987}
2988
2989#[gpui::test]
2990async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
2991 init_test_with_editor(cx);
2992 let fs = FakeFs::new(cx.executor());
2993 fs.insert_tree(
2994 "/project_root",
2995 json!({
2996 "dir_1": {
2997 "nested_dir": {
2998 "file_a.py": "# File contents",
2999 }
3000 },
3001 "file_1.py": "# File contents",
3002 }),
3003 )
3004 .await;
3005
3006 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3007 let worktree_id = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
3008 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3009 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3010 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3011 cx.update(|window, cx| {
3012 panel.update(cx, |this, cx| {
3013 this.select_next(&Default::default(), window, cx);
3014 this.expand_selected_entry(&Default::default(), window, cx);
3015 this.expand_selected_entry(&Default::default(), window, cx);
3016 this.select_next(&Default::default(), window, cx);
3017 this.expand_selected_entry(&Default::default(), window, cx);
3018 this.select_next(&Default::default(), window, cx);
3019 })
3020 });
3021 assert_eq!(
3022 visible_entries_as_strings(&panel, 0..10, cx),
3023 &[
3024 "v project_root",
3025 " v dir_1",
3026 " v nested_dir",
3027 " file_a.py <== selected",
3028 " file_1.py",
3029 ]
3030 );
3031 let modifiers_with_shift = gpui::Modifiers {
3032 shift: true,
3033 ..Default::default()
3034 };
3035 cx.run_until_parked();
3036 cx.simulate_modifiers_change(modifiers_with_shift);
3037 cx.update(|window, cx| {
3038 panel.update(cx, |this, cx| {
3039 this.select_next(&Default::default(), window, cx);
3040 })
3041 });
3042 assert_eq!(
3043 visible_entries_as_strings(&panel, 0..10, cx),
3044 &[
3045 "v project_root",
3046 " v dir_1",
3047 " v nested_dir",
3048 " file_a.py",
3049 " file_1.py <== selected <== marked",
3050 ]
3051 );
3052 cx.update(|window, cx| {
3053 panel.update(cx, |this, cx| {
3054 this.select_previous(&Default::default(), window, cx);
3055 })
3056 });
3057 assert_eq!(
3058 visible_entries_as_strings(&panel, 0..10, cx),
3059 &[
3060 "v project_root",
3061 " v dir_1",
3062 " v nested_dir",
3063 " file_a.py <== selected <== marked",
3064 " file_1.py <== marked",
3065 ]
3066 );
3067 cx.update(|window, cx| {
3068 panel.update(cx, |this, cx| {
3069 let drag = DraggedSelection {
3070 active_selection: this.selection.unwrap(),
3071 marked_selections: this.marked_entries.clone().into(),
3072 };
3073 let target_entry = this
3074 .project
3075 .read(cx)
3076 .entry_for_path(&(worktree_id, "").into(), cx)
3077 .unwrap();
3078 this.drag_onto(&drag, target_entry.id, false, window, cx);
3079 });
3080 });
3081 cx.run_until_parked();
3082 assert_eq!(
3083 visible_entries_as_strings(&panel, 0..10, cx),
3084 &[
3085 "v project_root",
3086 " v dir_1",
3087 " v nested_dir",
3088 " file_1.py <== marked",
3089 " file_a.py <== selected <== marked",
3090 ]
3091 );
3092 // ESC clears out all marks
3093 cx.update(|window, cx| {
3094 panel.update(cx, |this, cx| {
3095 this.cancel(&menu::Cancel, window, cx);
3096 })
3097 });
3098 assert_eq!(
3099 visible_entries_as_strings(&panel, 0..10, cx),
3100 &[
3101 "v project_root",
3102 " v dir_1",
3103 " v nested_dir",
3104 " file_1.py",
3105 " file_a.py <== selected",
3106 ]
3107 );
3108 // ESC clears out all marks
3109 cx.update(|window, cx| {
3110 panel.update(cx, |this, cx| {
3111 this.select_previous(&SelectPrevious, window, cx);
3112 this.select_next(&SelectNext, window, cx);
3113 })
3114 });
3115 assert_eq!(
3116 visible_entries_as_strings(&panel, 0..10, cx),
3117 &[
3118 "v project_root",
3119 " v dir_1",
3120 " v nested_dir",
3121 " file_1.py <== marked",
3122 " file_a.py <== selected <== marked",
3123 ]
3124 );
3125 cx.simulate_modifiers_change(Default::default());
3126 cx.update(|window, cx| {
3127 panel.update(cx, |this, cx| {
3128 this.cut(&Cut, window, cx);
3129 this.select_previous(&SelectPrevious, window, cx);
3130 this.select_previous(&SelectPrevious, window, cx);
3131
3132 this.paste(&Paste, window, cx);
3133 // this.expand_selected_entry(&ExpandSelectedEntry, cx);
3134 })
3135 });
3136 cx.run_until_parked();
3137 assert_eq!(
3138 visible_entries_as_strings(&panel, 0..10, cx),
3139 &[
3140 "v project_root",
3141 " v dir_1",
3142 " v nested_dir",
3143 " file_1.py <== marked",
3144 " file_a.py <== selected <== marked",
3145 ]
3146 );
3147 cx.simulate_modifiers_change(modifiers_with_shift);
3148 cx.update(|window, cx| {
3149 panel.update(cx, |this, cx| {
3150 this.expand_selected_entry(&Default::default(), window, cx);
3151 this.select_next(&SelectNext, window, cx);
3152 this.select_next(&SelectNext, window, cx);
3153 })
3154 });
3155 submit_deletion(&panel, cx);
3156 assert_eq!(
3157 visible_entries_as_strings(&panel, 0..10, cx),
3158 &[
3159 "v project_root",
3160 " v dir_1",
3161 " v nested_dir <== selected",
3162 ]
3163 );
3164}
3165
3166#[gpui::test]
3167async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
3168 init_test(cx);
3169
3170 let fs = FakeFs::new(cx.executor());
3171 fs.insert_tree(
3172 "/root",
3173 json!({
3174 "a": {
3175 "b": {
3176 "c": {
3177 "d": {}
3178 }
3179 }
3180 },
3181 "target_destination": {}
3182 }),
3183 )
3184 .await;
3185
3186 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
3187 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3188 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3189
3190 cx.update(|_, cx| {
3191 let settings = *ProjectPanelSettings::get_global(cx);
3192 ProjectPanelSettings::override_global(
3193 ProjectPanelSettings {
3194 auto_fold_dirs: true,
3195 ..settings
3196 },
3197 cx,
3198 );
3199 });
3200
3201 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3202
3203 // Case 1: Move last dir 'd' - should move only 'd', leaving 'a/b/c'
3204 select_path(&panel, "root/a/b/c/d", cx);
3205 panel.update_in(cx, |panel, window, cx| {
3206 let drag = DraggedSelection {
3207 active_selection: SelectedEntry {
3208 worktree_id: panel.selection.as_ref().unwrap().worktree_id,
3209 entry_id: panel.resolve_entry(panel.selection.as_ref().unwrap().entry_id),
3210 },
3211 marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
3212 };
3213 let target_entry = panel
3214 .project
3215 .read(cx)
3216 .visible_worktrees(cx)
3217 .next()
3218 .unwrap()
3219 .read(cx)
3220 .entry_for_path("target_destination")
3221 .unwrap();
3222 panel.drag_onto(&drag, target_entry.id, false, window, cx);
3223 });
3224 cx.executor().run_until_parked();
3225
3226 assert_eq!(
3227 visible_entries_as_strings(&panel, 0..10, cx),
3228 &[
3229 "v root",
3230 " > a/b/c",
3231 " > target_destination/d <== selected"
3232 ],
3233 "Moving last empty directory 'd' should leave 'a/b/c' and move only 'd'"
3234 );
3235
3236 // Reset
3237 select_path(&panel, "root/target_destination/d", cx);
3238 panel.update_in(cx, |panel, window, cx| {
3239 let drag = DraggedSelection {
3240 active_selection: SelectedEntry {
3241 worktree_id: panel.selection.as_ref().unwrap().worktree_id,
3242 entry_id: panel.resolve_entry(panel.selection.as_ref().unwrap().entry_id),
3243 },
3244 marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
3245 };
3246 let target_entry = panel
3247 .project
3248 .read(cx)
3249 .visible_worktrees(cx)
3250 .next()
3251 .unwrap()
3252 .read(cx)
3253 .entry_for_path("a/b/c")
3254 .unwrap();
3255 panel.drag_onto(&drag, target_entry.id, false, window, cx);
3256 });
3257 cx.executor().run_until_parked();
3258
3259 // Case 2: Move middle dir 'b' - should move 'b/c/d', leaving only 'a'
3260 select_path(&panel, "root/a/b", cx);
3261 panel.update_in(cx, |panel, window, cx| {
3262 let drag = DraggedSelection {
3263 active_selection: SelectedEntry {
3264 worktree_id: panel.selection.as_ref().unwrap().worktree_id,
3265 entry_id: panel.resolve_entry(panel.selection.as_ref().unwrap().entry_id),
3266 },
3267 marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
3268 };
3269 let target_entry = panel
3270 .project
3271 .read(cx)
3272 .visible_worktrees(cx)
3273 .next()
3274 .unwrap()
3275 .read(cx)
3276 .entry_for_path("target_destination")
3277 .unwrap();
3278 panel.drag_onto(&drag, target_entry.id, false, window, cx);
3279 });
3280 cx.executor().run_until_parked();
3281
3282 assert_eq!(
3283 visible_entries_as_strings(&panel, 0..10, cx),
3284 &["v root", " v a", " > target_destination/b/c/d"],
3285 "Moving middle directory 'b' should leave only 'a' and move 'b/c/d'"
3286 );
3287
3288 // Reset
3289 select_path(&panel, "root/target_destination/b", cx);
3290 panel.update_in(cx, |panel, window, cx| {
3291 let drag = DraggedSelection {
3292 active_selection: SelectedEntry {
3293 worktree_id: panel.selection.as_ref().unwrap().worktree_id,
3294 entry_id: panel.resolve_entry(panel.selection.as_ref().unwrap().entry_id),
3295 },
3296 marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
3297 };
3298 let target_entry = panel
3299 .project
3300 .read(cx)
3301 .visible_worktrees(cx)
3302 .next()
3303 .unwrap()
3304 .read(cx)
3305 .entry_for_path("a")
3306 .unwrap();
3307 panel.drag_onto(&drag, target_entry.id, false, window, cx);
3308 });
3309 cx.executor().run_until_parked();
3310
3311 // Case 3: Move first dir 'a' - should move whole 'a/b/c/d'
3312 select_path(&panel, "root/a", cx);
3313 panel.update_in(cx, |panel, window, cx| {
3314 let drag = DraggedSelection {
3315 active_selection: SelectedEntry {
3316 worktree_id: panel.selection.as_ref().unwrap().worktree_id,
3317 entry_id: panel.resolve_entry(panel.selection.as_ref().unwrap().entry_id),
3318 },
3319 marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
3320 };
3321 let target_entry = panel
3322 .project
3323 .read(cx)
3324 .visible_worktrees(cx)
3325 .next()
3326 .unwrap()
3327 .read(cx)
3328 .entry_for_path("target_destination")
3329 .unwrap();
3330 panel.drag_onto(&drag, target_entry.id, false, window, cx);
3331 });
3332 cx.executor().run_until_parked();
3333
3334 assert_eq!(
3335 visible_entries_as_strings(&panel, 0..10, cx),
3336 &["v root", " > target_destination/a/b/c/d"],
3337 "Moving first directory 'a' should move whole 'a/b/c/d' chain"
3338 );
3339}
3340
3341#[gpui::test]
3342async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
3343 init_test_with_editor(cx);
3344 cx.update(|cx| {
3345 cx.update_global::<SettingsStore, _>(|store, cx| {
3346 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3347 worktree_settings.file_scan_exclusions = Some(Vec::new());
3348 });
3349 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3350 project_panel_settings.auto_reveal_entries = Some(false)
3351 });
3352 })
3353 });
3354
3355 let fs = FakeFs::new(cx.background_executor.clone());
3356 fs.insert_tree(
3357 "/project_root",
3358 json!({
3359 ".git": {},
3360 ".gitignore": "**/gitignored_dir",
3361 "dir_1": {
3362 "file_1.py": "# File 1_1 contents",
3363 "file_2.py": "# File 1_2 contents",
3364 "file_3.py": "# File 1_3 contents",
3365 "gitignored_dir": {
3366 "file_a.py": "# File contents",
3367 "file_b.py": "# File contents",
3368 "file_c.py": "# File contents",
3369 },
3370 },
3371 "dir_2": {
3372 "file_1.py": "# File 2_1 contents",
3373 "file_2.py": "# File 2_2 contents",
3374 "file_3.py": "# File 2_3 contents",
3375 }
3376 }),
3377 )
3378 .await;
3379
3380 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3381 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3382 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3383 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3384
3385 assert_eq!(
3386 visible_entries_as_strings(&panel, 0..20, cx),
3387 &[
3388 "v project_root",
3389 " > .git",
3390 " > dir_1",
3391 " > dir_2",
3392 " .gitignore",
3393 ]
3394 );
3395
3396 let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
3397 .expect("dir 1 file is not ignored and should have an entry");
3398 let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
3399 .expect("dir 2 file is not ignored and should have an entry");
3400 let gitignored_dir_file =
3401 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3402 assert_eq!(
3403 gitignored_dir_file, None,
3404 "File in the gitignored dir should not have an entry before its dir is toggled"
3405 );
3406
3407 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3408 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3409 cx.executor().run_until_parked();
3410 assert_eq!(
3411 visible_entries_as_strings(&panel, 0..20, cx),
3412 &[
3413 "v project_root",
3414 " > .git",
3415 " v dir_1",
3416 " v gitignored_dir <== selected",
3417 " file_a.py",
3418 " file_b.py",
3419 " file_c.py",
3420 " file_1.py",
3421 " file_2.py",
3422 " file_3.py",
3423 " > dir_2",
3424 " .gitignore",
3425 ],
3426 "Should show gitignored dir file list in the project panel"
3427 );
3428 let gitignored_dir_file =
3429 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
3430 .expect("after gitignored dir got opened, a file entry should be present");
3431
3432 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3433 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3434 assert_eq!(
3435 visible_entries_as_strings(&panel, 0..20, cx),
3436 &[
3437 "v project_root",
3438 " > .git",
3439 " > dir_1 <== selected",
3440 " > dir_2",
3441 " .gitignore",
3442 ],
3443 "Should hide all dir contents again and prepare for the auto reveal test"
3444 );
3445
3446 for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
3447 panel.update(cx, |panel, cx| {
3448 panel.project.update(cx, |_, cx| {
3449 cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
3450 })
3451 });
3452 cx.run_until_parked();
3453 assert_eq!(
3454 visible_entries_as_strings(&panel, 0..20, cx),
3455 &[
3456 "v project_root",
3457 " > .git",
3458 " > dir_1 <== selected",
3459 " > dir_2",
3460 " .gitignore",
3461 ],
3462 "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
3463 );
3464 }
3465
3466 cx.update(|_, cx| {
3467 cx.update_global::<SettingsStore, _>(|store, cx| {
3468 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3469 project_panel_settings.auto_reveal_entries = Some(true)
3470 });
3471 })
3472 });
3473
3474 panel.update(cx, |panel, cx| {
3475 panel.project.update(cx, |_, cx| {
3476 cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
3477 })
3478 });
3479 cx.run_until_parked();
3480 assert_eq!(
3481 visible_entries_as_strings(&panel, 0..20, cx),
3482 &[
3483 "v project_root",
3484 " > .git",
3485 " v dir_1",
3486 " > gitignored_dir",
3487 " file_1.py <== selected <== marked",
3488 " file_2.py",
3489 " file_3.py",
3490 " > dir_2",
3491 " .gitignore",
3492 ],
3493 "When auto reveal is enabled, not ignored dir_1 entry should be revealed"
3494 );
3495
3496 panel.update(cx, |panel, cx| {
3497 panel.project.update(cx, |_, cx| {
3498 cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
3499 })
3500 });
3501 cx.run_until_parked();
3502 assert_eq!(
3503 visible_entries_as_strings(&panel, 0..20, cx),
3504 &[
3505 "v project_root",
3506 " > .git",
3507 " v dir_1",
3508 " > gitignored_dir",
3509 " file_1.py",
3510 " file_2.py",
3511 " file_3.py",
3512 " v dir_2",
3513 " file_1.py <== selected <== marked",
3514 " file_2.py",
3515 " file_3.py",
3516 " .gitignore",
3517 ],
3518 "When auto reveal is enabled, not ignored dir_2 entry should be revealed"
3519 );
3520
3521 panel.update(cx, |panel, cx| {
3522 panel.project.update(cx, |_, cx| {
3523 cx.emit(project::Event::ActiveEntryChanged(Some(
3524 gitignored_dir_file,
3525 )))
3526 })
3527 });
3528 cx.run_until_parked();
3529 assert_eq!(
3530 visible_entries_as_strings(&panel, 0..20, cx),
3531 &[
3532 "v project_root",
3533 " > .git",
3534 " v dir_1",
3535 " > gitignored_dir",
3536 " file_1.py",
3537 " file_2.py",
3538 " file_3.py",
3539 " v dir_2",
3540 " file_1.py <== selected <== marked",
3541 " file_2.py",
3542 " file_3.py",
3543 " .gitignore",
3544 ],
3545 "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
3546 );
3547
3548 panel.update(cx, |panel, cx| {
3549 panel.project.update(cx, |_, cx| {
3550 cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
3551 })
3552 });
3553 cx.run_until_parked();
3554 assert_eq!(
3555 visible_entries_as_strings(&panel, 0..20, cx),
3556 &[
3557 "v project_root",
3558 " > .git",
3559 " v dir_1",
3560 " v gitignored_dir",
3561 " file_a.py <== selected <== marked",
3562 " file_b.py",
3563 " file_c.py",
3564 " file_1.py",
3565 " file_2.py",
3566 " file_3.py",
3567 " v dir_2",
3568 " file_1.py",
3569 " file_2.py",
3570 " file_3.py",
3571 " .gitignore",
3572 ],
3573 "When a gitignored entry is explicitly revealed, it should be shown in the project tree"
3574 );
3575}
3576
3577#[gpui::test]
3578async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
3579 init_test_with_editor(cx);
3580 cx.update(|cx| {
3581 cx.update_global::<SettingsStore, _>(|store, cx| {
3582 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3583 worktree_settings.file_scan_exclusions = Some(Vec::new());
3584 worktree_settings.file_scan_inclusions =
3585 Some(vec!["always_included_but_ignored_dir/*".to_string()]);
3586 });
3587 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3588 project_panel_settings.auto_reveal_entries = Some(false)
3589 });
3590 })
3591 });
3592
3593 let fs = FakeFs::new(cx.background_executor.clone());
3594 fs.insert_tree(
3595 "/project_root",
3596 json!({
3597 ".git": {},
3598 ".gitignore": "**/gitignored_dir\n/always_included_but_ignored_dir",
3599 "dir_1": {
3600 "file_1.py": "# File 1_1 contents",
3601 "file_2.py": "# File 1_2 contents",
3602 "file_3.py": "# File 1_3 contents",
3603 "gitignored_dir": {
3604 "file_a.py": "# File contents",
3605 "file_b.py": "# File contents",
3606 "file_c.py": "# File contents",
3607 },
3608 },
3609 "dir_2": {
3610 "file_1.py": "# File 2_1 contents",
3611 "file_2.py": "# File 2_2 contents",
3612 "file_3.py": "# File 2_3 contents",
3613 },
3614 "always_included_but_ignored_dir": {
3615 "file_a.py": "# File contents",
3616 "file_b.py": "# File contents",
3617 "file_c.py": "# File contents",
3618 },
3619 }),
3620 )
3621 .await;
3622
3623 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3624 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3625 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3626 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3627
3628 assert_eq!(
3629 visible_entries_as_strings(&panel, 0..20, cx),
3630 &[
3631 "v project_root",
3632 " > .git",
3633 " > always_included_but_ignored_dir",
3634 " > dir_1",
3635 " > dir_2",
3636 " .gitignore",
3637 ]
3638 );
3639
3640 let gitignored_dir_file =
3641 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3642 let always_included_but_ignored_dir_file = find_project_entry(
3643 &panel,
3644 "project_root/always_included_but_ignored_dir/file_a.py",
3645 cx,
3646 )
3647 .expect("file that is .gitignored but set to always be included should have an entry");
3648 assert_eq!(
3649 gitignored_dir_file, None,
3650 "File in the gitignored dir should not have an entry unless its directory is toggled"
3651 );
3652
3653 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3654 cx.run_until_parked();
3655 cx.update(|_, cx| {
3656 cx.update_global::<SettingsStore, _>(|store, cx| {
3657 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3658 project_panel_settings.auto_reveal_entries = Some(true)
3659 });
3660 })
3661 });
3662
3663 panel.update(cx, |panel, cx| {
3664 panel.project.update(cx, |_, cx| {
3665 cx.emit(project::Event::ActiveEntryChanged(Some(
3666 always_included_but_ignored_dir_file,
3667 )))
3668 })
3669 });
3670 cx.run_until_parked();
3671
3672 assert_eq!(
3673 visible_entries_as_strings(&panel, 0..20, cx),
3674 &[
3675 "v project_root",
3676 " > .git",
3677 " v always_included_but_ignored_dir",
3678 " file_a.py <== selected <== marked",
3679 " file_b.py",
3680 " file_c.py",
3681 " v dir_1",
3682 " > gitignored_dir",
3683 " file_1.py",
3684 " file_2.py",
3685 " file_3.py",
3686 " > dir_2",
3687 " .gitignore",
3688 ],
3689 "When auto reveal is enabled, a gitignored but always included selected entry should be revealed in the project panel"
3690 );
3691}
3692
3693#[gpui::test]
3694async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
3695 init_test_with_editor(cx);
3696 cx.update(|cx| {
3697 cx.update_global::<SettingsStore, _>(|store, cx| {
3698 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3699 worktree_settings.file_scan_exclusions = Some(Vec::new());
3700 });
3701 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
3702 project_panel_settings.auto_reveal_entries = Some(false)
3703 });
3704 })
3705 });
3706
3707 let fs = FakeFs::new(cx.background_executor.clone());
3708 fs.insert_tree(
3709 "/project_root",
3710 json!({
3711 ".git": {},
3712 ".gitignore": "**/gitignored_dir",
3713 "dir_1": {
3714 "file_1.py": "# File 1_1 contents",
3715 "file_2.py": "# File 1_2 contents",
3716 "file_3.py": "# File 1_3 contents",
3717 "gitignored_dir": {
3718 "file_a.py": "# File contents",
3719 "file_b.py": "# File contents",
3720 "file_c.py": "# File contents",
3721 },
3722 },
3723 "dir_2": {
3724 "file_1.py": "# File 2_1 contents",
3725 "file_2.py": "# File 2_2 contents",
3726 "file_3.py": "# File 2_3 contents",
3727 }
3728 }),
3729 )
3730 .await;
3731
3732 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3733 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3734 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3735 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3736
3737 assert_eq!(
3738 visible_entries_as_strings(&panel, 0..20, cx),
3739 &[
3740 "v project_root",
3741 " > .git",
3742 " > dir_1",
3743 " > dir_2",
3744 " .gitignore",
3745 ]
3746 );
3747
3748 let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
3749 .expect("dir 1 file is not ignored and should have an entry");
3750 let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
3751 .expect("dir 2 file is not ignored and should have an entry");
3752 let gitignored_dir_file =
3753 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
3754 assert_eq!(
3755 gitignored_dir_file, None,
3756 "File in the gitignored dir should not have an entry before its dir is toggled"
3757 );
3758
3759 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3760 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3761 cx.run_until_parked();
3762 assert_eq!(
3763 visible_entries_as_strings(&panel, 0..20, cx),
3764 &[
3765 "v project_root",
3766 " > .git",
3767 " v dir_1",
3768 " v gitignored_dir <== selected",
3769 " file_a.py",
3770 " file_b.py",
3771 " file_c.py",
3772 " file_1.py",
3773 " file_2.py",
3774 " file_3.py",
3775 " > dir_2",
3776 " .gitignore",
3777 ],
3778 "Should show gitignored dir file list in the project panel"
3779 );
3780 let gitignored_dir_file =
3781 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
3782 .expect("after gitignored dir got opened, a file entry should be present");
3783
3784 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
3785 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3786 assert_eq!(
3787 visible_entries_as_strings(&panel, 0..20, cx),
3788 &[
3789 "v project_root",
3790 " > .git",
3791 " > dir_1 <== selected",
3792 " > dir_2",
3793 " .gitignore",
3794 ],
3795 "Should hide all dir contents again and prepare for the explicit reveal test"
3796 );
3797
3798 for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
3799 panel.update(cx, |panel, cx| {
3800 panel.project.update(cx, |_, cx| {
3801 cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
3802 })
3803 });
3804 cx.run_until_parked();
3805 assert_eq!(
3806 visible_entries_as_strings(&panel, 0..20, cx),
3807 &[
3808 "v project_root",
3809 " > .git",
3810 " > dir_1 <== selected",
3811 " > dir_2",
3812 " .gitignore",
3813 ],
3814 "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
3815 );
3816 }
3817
3818 panel.update(cx, |panel, cx| {
3819 panel.project.update(cx, |_, cx| {
3820 cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
3821 })
3822 });
3823 cx.run_until_parked();
3824 assert_eq!(
3825 visible_entries_as_strings(&panel, 0..20, cx),
3826 &[
3827 "v project_root",
3828 " > .git",
3829 " v dir_1",
3830 " > gitignored_dir",
3831 " file_1.py <== selected <== marked",
3832 " file_2.py",
3833 " file_3.py",
3834 " > dir_2",
3835 " .gitignore",
3836 ],
3837 "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
3838 );
3839
3840 panel.update(cx, |panel, cx| {
3841 panel.project.update(cx, |_, cx| {
3842 cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
3843 })
3844 });
3845 cx.run_until_parked();
3846 assert_eq!(
3847 visible_entries_as_strings(&panel, 0..20, cx),
3848 &[
3849 "v project_root",
3850 " > .git",
3851 " v dir_1",
3852 " > gitignored_dir",
3853 " file_1.py",
3854 " file_2.py",
3855 " file_3.py",
3856 " v dir_2",
3857 " file_1.py <== selected <== marked",
3858 " file_2.py",
3859 " file_3.py",
3860 " .gitignore",
3861 ],
3862 "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
3863 );
3864
3865 panel.update(cx, |panel, cx| {
3866 panel.project.update(cx, |_, cx| {
3867 cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
3868 })
3869 });
3870 cx.run_until_parked();
3871 assert_eq!(
3872 visible_entries_as_strings(&panel, 0..20, cx),
3873 &[
3874 "v project_root",
3875 " > .git",
3876 " v dir_1",
3877 " v gitignored_dir",
3878 " file_a.py <== selected <== marked",
3879 " file_b.py",
3880 " file_c.py",
3881 " file_1.py",
3882 " file_2.py",
3883 " file_3.py",
3884 " v dir_2",
3885 " file_1.py",
3886 " file_2.py",
3887 " file_3.py",
3888 " .gitignore",
3889 ],
3890 "With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
3891 );
3892}
3893
3894#[gpui::test]
3895async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
3896 init_test(cx);
3897 cx.update(|cx| {
3898 cx.update_global::<SettingsStore, _>(|store, cx| {
3899 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
3900 project_settings.file_scan_exclusions =
3901 Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
3902 });
3903 });
3904 });
3905
3906 cx.update(|cx| {
3907 register_project_item::<TestProjectItemView>(cx);
3908 });
3909
3910 let fs = FakeFs::new(cx.executor());
3911 fs.insert_tree(
3912 "/root1",
3913 json!({
3914 ".dockerignore": "",
3915 ".git": {
3916 "HEAD": "",
3917 },
3918 }),
3919 )
3920 .await;
3921
3922 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
3923 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3924 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3925 let panel = workspace
3926 .update(cx, |workspace, window, cx| {
3927 let panel = ProjectPanel::new(workspace, window, cx);
3928 workspace.add_panel(panel.clone(), window, cx);
3929 panel
3930 })
3931 .unwrap();
3932
3933 select_path(&panel, "root1", cx);
3934 assert_eq!(
3935 visible_entries_as_strings(&panel, 0..10, cx),
3936 &["v root1 <== selected", " .dockerignore",]
3937 );
3938 workspace
3939 .update(cx, |workspace, _, cx| {
3940 assert!(
3941 workspace.active_item(cx).is_none(),
3942 "Should have no active items in the beginning"
3943 );
3944 })
3945 .unwrap();
3946
3947 let excluded_file_path = ".git/COMMIT_EDITMSG";
3948 let excluded_dir_path = "excluded_dir";
3949
3950 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
3951 panel.update_in(cx, |panel, window, cx| {
3952 assert!(panel.filename_editor.read(cx).is_focused(window));
3953 });
3954 panel
3955 .update_in(cx, |panel, window, cx| {
3956 panel.filename_editor.update(cx, |editor, cx| {
3957 editor.set_text(excluded_file_path, window, cx)
3958 });
3959 panel.confirm_edit(window, cx).unwrap()
3960 })
3961 .await
3962 .unwrap();
3963
3964 assert_eq!(
3965 visible_entries_as_strings(&panel, 0..13, cx),
3966 &["v root1", " .dockerignore"],
3967 "Excluded dir should not be shown after opening a file in it"
3968 );
3969 panel.update_in(cx, |panel, window, cx| {
3970 assert!(
3971 !panel.filename_editor.read(cx).is_focused(window),
3972 "Should have closed the file name editor"
3973 );
3974 });
3975 workspace
3976 .update(cx, |workspace, _, cx| {
3977 let active_entry_path = workspace
3978 .active_item(cx)
3979 .expect("should have opened and activated the excluded item")
3980 .act_as::<TestProjectItemView>(cx)
3981 .expect("should have opened the corresponding project item for the excluded item")
3982 .read(cx)
3983 .path
3984 .clone();
3985 assert_eq!(
3986 active_entry_path.path.as_ref(),
3987 Path::new(excluded_file_path),
3988 "Should open the excluded file"
3989 );
3990
3991 assert!(
3992 workspace.notification_ids().is_empty(),
3993 "Should have no notifications after opening an excluded file"
3994 );
3995 })
3996 .unwrap();
3997 assert!(
3998 fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
3999 "Should have created the excluded file"
4000 );
4001
4002 select_path(&panel, "root1", cx);
4003 panel.update_in(cx, |panel, window, cx| {
4004 panel.new_directory(&NewDirectory, window, cx)
4005 });
4006 panel.update_in(cx, |panel, window, cx| {
4007 assert!(panel.filename_editor.read(cx).is_focused(window));
4008 });
4009 panel
4010 .update_in(cx, |panel, window, cx| {
4011 panel.filename_editor.update(cx, |editor, cx| {
4012 editor.set_text(excluded_file_path, window, cx)
4013 });
4014 panel.confirm_edit(window, cx).unwrap()
4015 })
4016 .await
4017 .unwrap();
4018
4019 assert_eq!(
4020 visible_entries_as_strings(&panel, 0..13, cx),
4021 &["v root1", " .dockerignore"],
4022 "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
4023 );
4024 panel.update_in(cx, |panel, window, cx| {
4025 assert!(
4026 !panel.filename_editor.read(cx).is_focused(window),
4027 "Should have closed the file name editor"
4028 );
4029 });
4030 workspace
4031 .update(cx, |workspace, _, cx| {
4032 let notifications = workspace.notification_ids();
4033 assert_eq!(
4034 notifications.len(),
4035 1,
4036 "Should receive one notification with the error message"
4037 );
4038 workspace.dismiss_notification(notifications.first().unwrap(), cx);
4039 assert!(workspace.notification_ids().is_empty());
4040 })
4041 .unwrap();
4042
4043 select_path(&panel, "root1", cx);
4044 panel.update_in(cx, |panel, window, cx| {
4045 panel.new_directory(&NewDirectory, window, cx)
4046 });
4047 panel.update_in(cx, |panel, window, cx| {
4048 assert!(panel.filename_editor.read(cx).is_focused(window));
4049 });
4050 panel
4051 .update_in(cx, |panel, window, cx| {
4052 panel.filename_editor.update(cx, |editor, cx| {
4053 editor.set_text(excluded_dir_path, window, cx)
4054 });
4055 panel.confirm_edit(window, cx).unwrap()
4056 })
4057 .await
4058 .unwrap();
4059
4060 assert_eq!(
4061 visible_entries_as_strings(&panel, 0..13, cx),
4062 &["v root1", " .dockerignore"],
4063 "Should not change the project panel after trying to create an excluded directory"
4064 );
4065 panel.update_in(cx, |panel, window, cx| {
4066 assert!(
4067 !panel.filename_editor.read(cx).is_focused(window),
4068 "Should have closed the file name editor"
4069 );
4070 });
4071 workspace
4072 .update(cx, |workspace, _, cx| {
4073 let notifications = workspace.notification_ids();
4074 assert_eq!(
4075 notifications.len(),
4076 1,
4077 "Should receive one notification explaining that no directory is actually shown"
4078 );
4079 workspace.dismiss_notification(notifications.first().unwrap(), cx);
4080 assert!(workspace.notification_ids().is_empty());
4081 })
4082 .unwrap();
4083 assert!(
4084 fs.is_dir(Path::new("/root1/excluded_dir")).await,
4085 "Should have created the excluded directory"
4086 );
4087}
4088
4089#[gpui::test]
4090async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) {
4091 init_test_with_editor(cx);
4092
4093 let fs = FakeFs::new(cx.executor());
4094 fs.insert_tree(
4095 "/src",
4096 json!({
4097 "test": {
4098 "first.rs": "// First Rust file",
4099 "second.rs": "// Second Rust file",
4100 "third.rs": "// Third Rust file",
4101 }
4102 }),
4103 )
4104 .await;
4105
4106 let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
4107 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4108 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4109 let panel = workspace
4110 .update(cx, |workspace, window, cx| {
4111 let panel = ProjectPanel::new(workspace, window, cx);
4112 workspace.add_panel(panel.clone(), window, cx);
4113 panel
4114 })
4115 .unwrap();
4116
4117 select_path(&panel, "src/", cx);
4118 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
4119 cx.executor().run_until_parked();
4120 assert_eq!(
4121 visible_entries_as_strings(&panel, 0..10, cx),
4122 &[
4123 //
4124 "v src <== selected",
4125 " > test"
4126 ]
4127 );
4128 panel.update_in(cx, |panel, window, cx| {
4129 panel.new_directory(&NewDirectory, window, cx)
4130 });
4131 panel.update_in(cx, |panel, window, cx| {
4132 assert!(panel.filename_editor.read(cx).is_focused(window));
4133 });
4134 assert_eq!(
4135 visible_entries_as_strings(&panel, 0..10, cx),
4136 &[
4137 //
4138 "v src",
4139 " > [EDITOR: ''] <== selected",
4140 " > test"
4141 ]
4142 );
4143
4144 panel.update_in(cx, |panel, window, cx| {
4145 panel.cancel(&menu::Cancel, window, cx)
4146 });
4147 assert_eq!(
4148 visible_entries_as_strings(&panel, 0..10, cx),
4149 &[
4150 //
4151 "v src <== selected",
4152 " > test"
4153 ]
4154 );
4155}
4156
4157#[gpui::test]
4158async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
4159 init_test_with_editor(cx);
4160
4161 let fs = FakeFs::new(cx.executor());
4162 fs.insert_tree(
4163 "/root",
4164 json!({
4165 "dir1": {
4166 "subdir1": {},
4167 "file1.txt": "",
4168 "file2.txt": "",
4169 },
4170 "dir2": {
4171 "subdir2": {},
4172 "file3.txt": "",
4173 "file4.txt": "",
4174 },
4175 "file5.txt": "",
4176 "file6.txt": "",
4177 }),
4178 )
4179 .await;
4180
4181 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4182 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4183 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4184 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4185
4186 toggle_expand_dir(&panel, "root/dir1", cx);
4187 toggle_expand_dir(&panel, "root/dir2", cx);
4188
4189 // Test Case 1: Delete middle file in directory
4190 select_path(&panel, "root/dir1/file1.txt", cx);
4191 assert_eq!(
4192 visible_entries_as_strings(&panel, 0..15, cx),
4193 &[
4194 "v root",
4195 " v dir1",
4196 " > subdir1",
4197 " file1.txt <== selected",
4198 " file2.txt",
4199 " v dir2",
4200 " > subdir2",
4201 " file3.txt",
4202 " file4.txt",
4203 " file5.txt",
4204 " file6.txt",
4205 ],
4206 "Initial state before deleting middle file"
4207 );
4208
4209 submit_deletion(&panel, cx);
4210 assert_eq!(
4211 visible_entries_as_strings(&panel, 0..15, cx),
4212 &[
4213 "v root",
4214 " v dir1",
4215 " > subdir1",
4216 " file2.txt <== selected",
4217 " v dir2",
4218 " > subdir2",
4219 " file3.txt",
4220 " file4.txt",
4221 " file5.txt",
4222 " file6.txt",
4223 ],
4224 "Should select next file after deleting middle file"
4225 );
4226
4227 // Test Case 2: Delete last file in directory
4228 submit_deletion(&panel, cx);
4229 assert_eq!(
4230 visible_entries_as_strings(&panel, 0..15, cx),
4231 &[
4232 "v root",
4233 " v dir1",
4234 " > subdir1 <== selected",
4235 " v dir2",
4236 " > subdir2",
4237 " file3.txt",
4238 " file4.txt",
4239 " file5.txt",
4240 " file6.txt",
4241 ],
4242 "Should select next directory when last file is deleted"
4243 );
4244
4245 // Test Case 3: Delete root level file
4246 select_path(&panel, "root/file6.txt", cx);
4247 assert_eq!(
4248 visible_entries_as_strings(&panel, 0..15, cx),
4249 &[
4250 "v root",
4251 " v dir1",
4252 " > subdir1",
4253 " v dir2",
4254 " > subdir2",
4255 " file3.txt",
4256 " file4.txt",
4257 " file5.txt",
4258 " file6.txt <== selected",
4259 ],
4260 "Initial state before deleting root level file"
4261 );
4262
4263 submit_deletion(&panel, cx);
4264 assert_eq!(
4265 visible_entries_as_strings(&panel, 0..15, cx),
4266 &[
4267 "v root",
4268 " v dir1",
4269 " > subdir1",
4270 " v dir2",
4271 " > subdir2",
4272 " file3.txt",
4273 " file4.txt",
4274 " file5.txt <== selected",
4275 ],
4276 "Should select prev entry at root level"
4277 );
4278}
4279
4280#[gpui::test]
4281async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) {
4282 init_test_with_editor(cx);
4283
4284 let fs = FakeFs::new(cx.executor());
4285 fs.insert_tree(
4286 path!("/root"),
4287 json!({
4288 "aa": "// Testing 1",
4289 "bb": "// Testing 2",
4290 "cc": "// Testing 3",
4291 "dd": "// Testing 4",
4292 "ee": "// Testing 5",
4293 "ff": "// Testing 6",
4294 "gg": "// Testing 7",
4295 "hh": "// Testing 8",
4296 "ii": "// Testing 8",
4297 ".gitignore": "bb\ndd\nee\nff\nii\n'",
4298 }),
4299 )
4300 .await;
4301
4302 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4303 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4304 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4305
4306 // Test 1: Auto selection with one gitignored file next to the deleted file
4307 cx.update(|_, cx| {
4308 let settings = *ProjectPanelSettings::get_global(cx);
4309 ProjectPanelSettings::override_global(
4310 ProjectPanelSettings {
4311 hide_gitignore: true,
4312 ..settings
4313 },
4314 cx,
4315 );
4316 });
4317
4318 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4319
4320 select_path(&panel, "root/aa", cx);
4321 assert_eq!(
4322 visible_entries_as_strings(&panel, 0..10, cx),
4323 &[
4324 "v root",
4325 " .gitignore",
4326 " aa <== selected",
4327 " cc",
4328 " gg",
4329 " hh"
4330 ],
4331 "Initial state should hide files on .gitignore"
4332 );
4333
4334 submit_deletion(&panel, cx);
4335
4336 assert_eq!(
4337 visible_entries_as_strings(&panel, 0..10, cx),
4338 &[
4339 "v root",
4340 " .gitignore",
4341 " cc <== selected",
4342 " gg",
4343 " hh"
4344 ],
4345 "Should select next entry not on .gitignore"
4346 );
4347
4348 // Test 2: Auto selection with many gitignored files next to the deleted file
4349 submit_deletion(&panel, cx);
4350 assert_eq!(
4351 visible_entries_as_strings(&panel, 0..10, cx),
4352 &[
4353 "v root",
4354 " .gitignore",
4355 " gg <== selected",
4356 " hh"
4357 ],
4358 "Should select next entry not on .gitignore"
4359 );
4360
4361 // Test 3: Auto selection of entry before deleted file
4362 select_path(&panel, "root/hh", cx);
4363 assert_eq!(
4364 visible_entries_as_strings(&panel, 0..10, cx),
4365 &[
4366 "v root",
4367 " .gitignore",
4368 " gg",
4369 " hh <== selected"
4370 ],
4371 "Should select next entry not on .gitignore"
4372 );
4373 submit_deletion(&panel, cx);
4374 assert_eq!(
4375 visible_entries_as_strings(&panel, 0..10, cx),
4376 &["v root", " .gitignore", " gg <== selected"],
4377 "Should select next entry not on .gitignore"
4378 );
4379}
4380
4381#[gpui::test]
4382async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) {
4383 init_test_with_editor(cx);
4384
4385 let fs = FakeFs::new(cx.executor());
4386 fs.insert_tree(
4387 path!("/root"),
4388 json!({
4389 "dir1": {
4390 "file1": "// Testing",
4391 "file2": "// Testing",
4392 "file3": "// Testing"
4393 },
4394 "aa": "// Testing",
4395 ".gitignore": "file1\nfile3\n",
4396 }),
4397 )
4398 .await;
4399
4400 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4401 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4402 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4403
4404 cx.update(|_, cx| {
4405 let settings = *ProjectPanelSettings::get_global(cx);
4406 ProjectPanelSettings::override_global(
4407 ProjectPanelSettings {
4408 hide_gitignore: true,
4409 ..settings
4410 },
4411 cx,
4412 );
4413 });
4414
4415 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4416
4417 // Test 1: Visible items should exclude files on gitignore
4418 toggle_expand_dir(&panel, "root/dir1", cx);
4419 select_path(&panel, "root/dir1/file2", cx);
4420 assert_eq!(
4421 visible_entries_as_strings(&panel, 0..10, cx),
4422 &[
4423 "v root",
4424 " v dir1",
4425 " file2 <== selected",
4426 " .gitignore",
4427 " aa"
4428 ],
4429 "Initial state should hide files on .gitignore"
4430 );
4431 submit_deletion(&panel, cx);
4432
4433 // Test 2: Auto selection should go to the parent
4434 assert_eq!(
4435 visible_entries_as_strings(&panel, 0..10, cx),
4436 &[
4437 "v root",
4438 " v dir1 <== selected",
4439 " .gitignore",
4440 " aa"
4441 ],
4442 "Initial state should hide files on .gitignore"
4443 );
4444}
4445
4446#[gpui::test]
4447async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
4448 init_test_with_editor(cx);
4449
4450 let fs = FakeFs::new(cx.executor());
4451 fs.insert_tree(
4452 "/root",
4453 json!({
4454 "dir1": {
4455 "subdir1": {
4456 "a.txt": "",
4457 "b.txt": ""
4458 },
4459 "file1.txt": "",
4460 },
4461 "dir2": {
4462 "subdir2": {
4463 "c.txt": "",
4464 "d.txt": ""
4465 },
4466 "file2.txt": "",
4467 },
4468 "file3.txt": "",
4469 }),
4470 )
4471 .await;
4472
4473 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4474 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4475 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4476 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4477
4478 toggle_expand_dir(&panel, "root/dir1", cx);
4479 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4480 toggle_expand_dir(&panel, "root/dir2", cx);
4481 toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
4482
4483 // Test Case 1: Select and delete nested directory with parent
4484 cx.simulate_modifiers_change(gpui::Modifiers {
4485 control: true,
4486 ..Default::default()
4487 });
4488 select_path_with_mark(&panel, "root/dir1/subdir1", cx);
4489 select_path_with_mark(&panel, "root/dir1", cx);
4490
4491 assert_eq!(
4492 visible_entries_as_strings(&panel, 0..15, cx),
4493 &[
4494 "v root",
4495 " v dir1 <== selected <== marked",
4496 " v subdir1 <== marked",
4497 " a.txt",
4498 " b.txt",
4499 " file1.txt",
4500 " v dir2",
4501 " v subdir2",
4502 " c.txt",
4503 " d.txt",
4504 " file2.txt",
4505 " file3.txt",
4506 ],
4507 "Initial state before deleting nested directory with parent"
4508 );
4509
4510 submit_deletion(&panel, cx);
4511 assert_eq!(
4512 visible_entries_as_strings(&panel, 0..15, cx),
4513 &[
4514 "v root",
4515 " v dir2 <== selected",
4516 " v subdir2",
4517 " c.txt",
4518 " d.txt",
4519 " file2.txt",
4520 " file3.txt",
4521 ],
4522 "Should select next directory after deleting directory with parent"
4523 );
4524
4525 // Test Case 2: Select mixed files and directories across levels
4526 select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx);
4527 select_path_with_mark(&panel, "root/dir2/file2.txt", cx);
4528 select_path_with_mark(&panel, "root/file3.txt", cx);
4529
4530 assert_eq!(
4531 visible_entries_as_strings(&panel, 0..15, cx),
4532 &[
4533 "v root",
4534 " v dir2",
4535 " v subdir2",
4536 " c.txt <== marked",
4537 " d.txt",
4538 " file2.txt <== marked",
4539 " file3.txt <== selected <== marked",
4540 ],
4541 "Initial state before deleting"
4542 );
4543
4544 submit_deletion(&panel, cx);
4545 assert_eq!(
4546 visible_entries_as_strings(&panel, 0..15, cx),
4547 &[
4548 "v root",
4549 " v dir2 <== selected",
4550 " v subdir2",
4551 " d.txt",
4552 ],
4553 "Should select sibling directory"
4554 );
4555}
4556
4557#[gpui::test]
4558async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) {
4559 init_test_with_editor(cx);
4560
4561 let fs = FakeFs::new(cx.executor());
4562 fs.insert_tree(
4563 "/root",
4564 json!({
4565 "dir1": {
4566 "subdir1": {
4567 "a.txt": "",
4568 "b.txt": ""
4569 },
4570 "file1.txt": "",
4571 },
4572 "dir2": {
4573 "subdir2": {
4574 "c.txt": "",
4575 "d.txt": ""
4576 },
4577 "file2.txt": "",
4578 },
4579 "file3.txt": "",
4580 "file4.txt": "",
4581 }),
4582 )
4583 .await;
4584
4585 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4586 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4587 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4588 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4589
4590 toggle_expand_dir(&panel, "root/dir1", cx);
4591 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4592 toggle_expand_dir(&panel, "root/dir2", cx);
4593 toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
4594
4595 // Test Case 1: Select all root files and directories
4596 cx.simulate_modifiers_change(gpui::Modifiers {
4597 control: true,
4598 ..Default::default()
4599 });
4600 select_path_with_mark(&panel, "root/dir1", cx);
4601 select_path_with_mark(&panel, "root/dir2", cx);
4602 select_path_with_mark(&panel, "root/file3.txt", cx);
4603 select_path_with_mark(&panel, "root/file4.txt", cx);
4604 assert_eq!(
4605 visible_entries_as_strings(&panel, 0..20, cx),
4606 &[
4607 "v root",
4608 " v dir1 <== marked",
4609 " v subdir1",
4610 " a.txt",
4611 " b.txt",
4612 " file1.txt",
4613 " v dir2 <== marked",
4614 " v subdir2",
4615 " c.txt",
4616 " d.txt",
4617 " file2.txt",
4618 " file3.txt <== marked",
4619 " file4.txt <== selected <== marked",
4620 ],
4621 "State before deleting all contents"
4622 );
4623
4624 submit_deletion(&panel, cx);
4625 assert_eq!(
4626 visible_entries_as_strings(&panel, 0..20, cx),
4627 &["v root <== selected"],
4628 "Only empty root directory should remain after deleting all contents"
4629 );
4630}
4631
4632#[gpui::test]
4633async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) {
4634 init_test_with_editor(cx);
4635
4636 let fs = FakeFs::new(cx.executor());
4637 fs.insert_tree(
4638 "/root",
4639 json!({
4640 "dir1": {
4641 "subdir1": {
4642 "file_a.txt": "content a",
4643 "file_b.txt": "content b",
4644 },
4645 "subdir2": {
4646 "file_c.txt": "content c",
4647 },
4648 "file1.txt": "content 1",
4649 },
4650 "dir2": {
4651 "file2.txt": "content 2",
4652 },
4653 }),
4654 )
4655 .await;
4656
4657 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4658 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4659 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4660 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4661
4662 toggle_expand_dir(&panel, "root/dir1", cx);
4663 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
4664 toggle_expand_dir(&panel, "root/dir2", cx);
4665 cx.simulate_modifiers_change(gpui::Modifiers {
4666 control: true,
4667 ..Default::default()
4668 });
4669
4670 // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory
4671 select_path_with_mark(&panel, "root/dir1", cx);
4672 select_path_with_mark(&panel, "root/dir1/subdir1", cx);
4673 select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx);
4674
4675 assert_eq!(
4676 visible_entries_as_strings(&panel, 0..20, cx),
4677 &[
4678 "v root",
4679 " v dir1 <== marked",
4680 " v subdir1 <== marked",
4681 " file_a.txt <== selected <== marked",
4682 " file_b.txt",
4683 " > subdir2",
4684 " file1.txt",
4685 " v dir2",
4686 " file2.txt",
4687 ],
4688 "State with parent dir, subdir, and file selected"
4689 );
4690 submit_deletion(&panel, cx);
4691 assert_eq!(
4692 visible_entries_as_strings(&panel, 0..20, cx),
4693 &["v root", " v dir2 <== selected", " file2.txt",],
4694 "Only dir2 should remain after deletion"
4695 );
4696}
4697
4698#[gpui::test]
4699async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) {
4700 init_test_with_editor(cx);
4701
4702 let fs = FakeFs::new(cx.executor());
4703 // First worktree
4704 fs.insert_tree(
4705 "/root1",
4706 json!({
4707 "dir1": {
4708 "file1.txt": "content 1",
4709 "file2.txt": "content 2",
4710 },
4711 "dir2": {
4712 "file3.txt": "content 3",
4713 },
4714 }),
4715 )
4716 .await;
4717
4718 // Second worktree
4719 fs.insert_tree(
4720 "/root2",
4721 json!({
4722 "dir3": {
4723 "file4.txt": "content 4",
4724 "file5.txt": "content 5",
4725 },
4726 "file6.txt": "content 6",
4727 }),
4728 )
4729 .await;
4730
4731 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
4732 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4733 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4734 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4735
4736 // Expand all directories for testing
4737 toggle_expand_dir(&panel, "root1/dir1", cx);
4738 toggle_expand_dir(&panel, "root1/dir2", cx);
4739 toggle_expand_dir(&panel, "root2/dir3", cx);
4740
4741 // Test Case 1: Delete files across different worktrees
4742 cx.simulate_modifiers_change(gpui::Modifiers {
4743 control: true,
4744 ..Default::default()
4745 });
4746 select_path_with_mark(&panel, "root1/dir1/file1.txt", cx);
4747 select_path_with_mark(&panel, "root2/dir3/file4.txt", cx);
4748
4749 assert_eq!(
4750 visible_entries_as_strings(&panel, 0..20, cx),
4751 &[
4752 "v root1",
4753 " v dir1",
4754 " file1.txt <== marked",
4755 " file2.txt",
4756 " v dir2",
4757 " file3.txt",
4758 "v root2",
4759 " v dir3",
4760 " file4.txt <== selected <== marked",
4761 " file5.txt",
4762 " file6.txt",
4763 ],
4764 "Initial state with files selected from different worktrees"
4765 );
4766
4767 submit_deletion(&panel, cx);
4768 assert_eq!(
4769 visible_entries_as_strings(&panel, 0..20, cx),
4770 &[
4771 "v root1",
4772 " v dir1",
4773 " file2.txt",
4774 " v dir2",
4775 " file3.txt",
4776 "v root2",
4777 " v dir3",
4778 " file5.txt <== selected",
4779 " file6.txt",
4780 ],
4781 "Should select next file in the last worktree after deletion"
4782 );
4783
4784 // Test Case 2: Delete directories from different worktrees
4785 select_path_with_mark(&panel, "root1/dir1", cx);
4786 select_path_with_mark(&panel, "root2/dir3", cx);
4787
4788 assert_eq!(
4789 visible_entries_as_strings(&panel, 0..20, cx),
4790 &[
4791 "v root1",
4792 " v dir1 <== marked",
4793 " file2.txt",
4794 " v dir2",
4795 " file3.txt",
4796 "v root2",
4797 " v dir3 <== selected <== marked",
4798 " file5.txt",
4799 " file6.txt",
4800 ],
4801 "State with directories marked from different worktrees"
4802 );
4803
4804 submit_deletion(&panel, cx);
4805 assert_eq!(
4806 visible_entries_as_strings(&panel, 0..20, cx),
4807 &[
4808 "v root1",
4809 " v dir2",
4810 " file3.txt",
4811 "v root2",
4812 " file6.txt <== selected",
4813 ],
4814 "Should select remaining file in last worktree after directory deletion"
4815 );
4816
4817 // Test Case 4: Delete all remaining files except roots
4818 select_path_with_mark(&panel, "root1/dir2/file3.txt", cx);
4819 select_path_with_mark(&panel, "root2/file6.txt", cx);
4820
4821 assert_eq!(
4822 visible_entries_as_strings(&panel, 0..20, cx),
4823 &[
4824 "v root1",
4825 " v dir2",
4826 " file3.txt <== marked",
4827 "v root2",
4828 " file6.txt <== selected <== marked",
4829 ],
4830 "State with all remaining files marked"
4831 );
4832
4833 submit_deletion(&panel, cx);
4834 assert_eq!(
4835 visible_entries_as_strings(&panel, 0..20, cx),
4836 &["v root1", " v dir2", "v root2 <== selected"],
4837 "Second parent root should be selected after deleting"
4838 );
4839}
4840
4841#[gpui::test]
4842async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) {
4843 init_test_with_editor(cx);
4844
4845 let fs = FakeFs::new(cx.executor());
4846 fs.insert_tree(
4847 "/root",
4848 json!({
4849 "dir1": {
4850 "file1.txt": "",
4851 "file2.txt": "",
4852 "file3.txt": "",
4853 },
4854 "dir2": {
4855 "file4.txt": "",
4856 "file5.txt": "",
4857 },
4858 }),
4859 )
4860 .await;
4861
4862 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4863 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4864 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4865 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4866
4867 toggle_expand_dir(&panel, "root/dir1", cx);
4868 toggle_expand_dir(&panel, "root/dir2", cx);
4869
4870 cx.simulate_modifiers_change(gpui::Modifiers {
4871 control: true,
4872 ..Default::default()
4873 });
4874
4875 select_path_with_mark(&panel, "root/dir1/file2.txt", cx);
4876 select_path(&panel, "root/dir1/file1.txt", cx);
4877
4878 assert_eq!(
4879 visible_entries_as_strings(&panel, 0..15, cx),
4880 &[
4881 "v root",
4882 " v dir1",
4883 " file1.txt <== selected",
4884 " file2.txt <== marked",
4885 " file3.txt",
4886 " v dir2",
4887 " file4.txt",
4888 " file5.txt",
4889 ],
4890 "Initial state with one marked entry and different selection"
4891 );
4892
4893 // Delete should operate on the selected entry (file1.txt)
4894 submit_deletion(&panel, cx);
4895 assert_eq!(
4896 visible_entries_as_strings(&panel, 0..15, cx),
4897 &[
4898 "v root",
4899 " v dir1",
4900 " file2.txt <== selected <== marked",
4901 " file3.txt",
4902 " v dir2",
4903 " file4.txt",
4904 " file5.txt",
4905 ],
4906 "Should delete selected file, not marked file"
4907 );
4908
4909 select_path_with_mark(&panel, "root/dir1/file3.txt", cx);
4910 select_path_with_mark(&panel, "root/dir2/file4.txt", cx);
4911 select_path(&panel, "root/dir2/file5.txt", cx);
4912
4913 assert_eq!(
4914 visible_entries_as_strings(&panel, 0..15, cx),
4915 &[
4916 "v root",
4917 " v dir1",
4918 " file2.txt <== marked",
4919 " file3.txt <== marked",
4920 " v dir2",
4921 " file4.txt <== marked",
4922 " file5.txt <== selected",
4923 ],
4924 "Initial state with multiple marked entries and different selection"
4925 );
4926
4927 // Delete should operate on all marked entries, ignoring the selection
4928 submit_deletion(&panel, cx);
4929 assert_eq!(
4930 visible_entries_as_strings(&panel, 0..15, cx),
4931 &[
4932 "v root",
4933 " v dir1",
4934 " v dir2",
4935 " file5.txt <== selected",
4936 ],
4937 "Should delete all marked files, leaving only the selected file"
4938 );
4939}
4940
4941#[gpui::test]
4942async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) {
4943 init_test_with_editor(cx);
4944
4945 let fs = FakeFs::new(cx.executor());
4946 fs.insert_tree(
4947 "/root_b",
4948 json!({
4949 "dir1": {
4950 "file1.txt": "content 1",
4951 "file2.txt": "content 2",
4952 },
4953 }),
4954 )
4955 .await;
4956
4957 fs.insert_tree(
4958 "/root_c",
4959 json!({
4960 "dir2": {},
4961 }),
4962 )
4963 .await;
4964
4965 let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await;
4966 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4967 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4968 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4969
4970 toggle_expand_dir(&panel, "root_b/dir1", cx);
4971 toggle_expand_dir(&panel, "root_c/dir2", cx);
4972
4973 cx.simulate_modifiers_change(gpui::Modifiers {
4974 control: true,
4975 ..Default::default()
4976 });
4977 select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx);
4978 select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx);
4979
4980 assert_eq!(
4981 visible_entries_as_strings(&panel, 0..20, cx),
4982 &[
4983 "v root_b",
4984 " v dir1",
4985 " file1.txt <== marked",
4986 " file2.txt <== selected <== marked",
4987 "v root_c",
4988 " v dir2",
4989 ],
4990 "Initial state with files marked in root_b"
4991 );
4992
4993 submit_deletion(&panel, cx);
4994 assert_eq!(
4995 visible_entries_as_strings(&panel, 0..20, cx),
4996 &[
4997 "v root_b",
4998 " v dir1 <== selected",
4999 "v root_c",
5000 " v dir2",
5001 ],
5002 "After deletion in root_b as it's last deletion, selection should be in root_b"
5003 );
5004
5005 select_path_with_mark(&panel, "root_c/dir2", cx);
5006
5007 submit_deletion(&panel, cx);
5008 assert_eq!(
5009 visible_entries_as_strings(&panel, 0..20, cx),
5010 &["v root_b", " v dir1", "v root_c <== selected",],
5011 "After deleting from root_c, it should remain in root_c"
5012 );
5013}
5014
5015fn toggle_expand_dir(
5016 panel: &Entity<ProjectPanel>,
5017 path: impl AsRef<Path>,
5018 cx: &mut VisualTestContext,
5019) {
5020 let path = path.as_ref();
5021 panel.update_in(cx, |panel, window, cx| {
5022 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
5023 let worktree = worktree.read(cx);
5024 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
5025 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
5026 panel.toggle_expanded(entry_id, window, cx);
5027 return;
5028 }
5029 }
5030 panic!("no worktree for path {:?}", path);
5031 });
5032}
5033
5034#[gpui::test]
5035async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) {
5036 init_test_with_editor(cx);
5037
5038 let fs = FakeFs::new(cx.executor());
5039 fs.insert_tree(
5040 path!("/root"),
5041 json!({
5042 ".gitignore": "**/ignored_dir\n**/ignored_nested",
5043 "dir1": {
5044 "empty1": {
5045 "empty2": {
5046 "empty3": {
5047 "file.txt": ""
5048 }
5049 }
5050 },
5051 "subdir1": {
5052 "file1.txt": "",
5053 "file2.txt": "",
5054 "ignored_nested": {
5055 "ignored_file.txt": ""
5056 }
5057 },
5058 "ignored_dir": {
5059 "subdir": {
5060 "deep_file.txt": ""
5061 }
5062 }
5063 }
5064 }),
5065 )
5066 .await;
5067
5068 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
5069 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5070 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5071
5072 // Test 1: When auto-fold is enabled
5073 cx.update(|_, cx| {
5074 let settings = *ProjectPanelSettings::get_global(cx);
5075 ProjectPanelSettings::override_global(
5076 ProjectPanelSettings {
5077 auto_fold_dirs: true,
5078 ..settings
5079 },
5080 cx,
5081 );
5082 });
5083
5084 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5085
5086 assert_eq!(
5087 visible_entries_as_strings(&panel, 0..20, cx),
5088 &["v root", " > dir1", " .gitignore",],
5089 "Initial state should show collapsed root structure"
5090 );
5091
5092 toggle_expand_dir(&panel, "root/dir1", cx);
5093 assert_eq!(
5094 visible_entries_as_strings(&panel, 0..20, cx),
5095 &[
5096 "v root",
5097 " v dir1 <== selected",
5098 " > empty1/empty2/empty3",
5099 " > ignored_dir",
5100 " > subdir1",
5101 " .gitignore",
5102 ],
5103 "Should show first level with auto-folded dirs and ignored dir visible"
5104 );
5105
5106 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
5107 panel.update(cx, |panel, cx| {
5108 let project = panel.project.read(cx);
5109 let worktree = project.worktrees(cx).next().unwrap().read(cx);
5110 panel.expand_all_for_entry(worktree.id(), entry_id, cx);
5111 panel.update_visible_entries(None, cx);
5112 });
5113 cx.run_until_parked();
5114
5115 assert_eq!(
5116 visible_entries_as_strings(&panel, 0..20, cx),
5117 &[
5118 "v root",
5119 " v dir1 <== selected",
5120 " v empty1",
5121 " v empty2",
5122 " v empty3",
5123 " file.txt",
5124 " > ignored_dir",
5125 " v subdir1",
5126 " > ignored_nested",
5127 " file1.txt",
5128 " file2.txt",
5129 " .gitignore",
5130 ],
5131 "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested"
5132 );
5133
5134 // Test 2: When auto-fold is disabled
5135 cx.update(|_, cx| {
5136 let settings = *ProjectPanelSettings::get_global(cx);
5137 ProjectPanelSettings::override_global(
5138 ProjectPanelSettings {
5139 auto_fold_dirs: false,
5140 ..settings
5141 },
5142 cx,
5143 );
5144 });
5145
5146 panel.update_in(cx, |panel, window, cx| {
5147 panel.collapse_all_entries(&CollapseAllEntries, window, cx);
5148 });
5149
5150 toggle_expand_dir(&panel, "root/dir1", cx);
5151 assert_eq!(
5152 visible_entries_as_strings(&panel, 0..20, cx),
5153 &[
5154 "v root",
5155 " v dir1 <== selected",
5156 " > empty1",
5157 " > ignored_dir",
5158 " > subdir1",
5159 " .gitignore",
5160 ],
5161 "With auto-fold disabled: should show all directories separately"
5162 );
5163
5164 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
5165 panel.update(cx, |panel, cx| {
5166 let project = panel.project.read(cx);
5167 let worktree = project.worktrees(cx).next().unwrap().read(cx);
5168 panel.expand_all_for_entry(worktree.id(), entry_id, cx);
5169 panel.update_visible_entries(None, cx);
5170 });
5171 cx.run_until_parked();
5172
5173 assert_eq!(
5174 visible_entries_as_strings(&panel, 0..20, cx),
5175 &[
5176 "v root",
5177 " v dir1 <== selected",
5178 " v empty1",
5179 " v empty2",
5180 " v empty3",
5181 " file.txt",
5182 " > ignored_dir",
5183 " v subdir1",
5184 " > ignored_nested",
5185 " file1.txt",
5186 " file2.txt",
5187 " .gitignore",
5188 ],
5189 "After expand_all without auto-fold: should expand all dirs normally, \
5190 expand ignored_dir itself but not its subdirs, and not expand ignored_nested"
5191 );
5192
5193 // Test 3: When explicitly called on ignored directory
5194 let ignored_dir_entry = find_project_entry(&panel, "root/dir1/ignored_dir", cx).unwrap();
5195 panel.update(cx, |panel, cx| {
5196 let project = panel.project.read(cx);
5197 let worktree = project.worktrees(cx).next().unwrap().read(cx);
5198 panel.expand_all_for_entry(worktree.id(), ignored_dir_entry, cx);
5199 panel.update_visible_entries(None, cx);
5200 });
5201 cx.run_until_parked();
5202
5203 assert_eq!(
5204 visible_entries_as_strings(&panel, 0..20, cx),
5205 &[
5206 "v root",
5207 " v dir1 <== selected",
5208 " v empty1",
5209 " v empty2",
5210 " v empty3",
5211 " file.txt",
5212 " v ignored_dir",
5213 " v subdir",
5214 " deep_file.txt",
5215 " v subdir1",
5216 " > ignored_nested",
5217 " file1.txt",
5218 " file2.txt",
5219 " .gitignore",
5220 ],
5221 "After expand_all on ignored_dir: should expand all contents of the ignored directory"
5222 );
5223}
5224
5225#[gpui::test]
5226async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) {
5227 init_test(cx);
5228
5229 let fs = FakeFs::new(cx.executor());
5230 fs.insert_tree(
5231 path!("/root"),
5232 json!({
5233 "dir1": {
5234 "subdir1": {
5235 "nested1": {
5236 "file1.txt": "",
5237 "file2.txt": ""
5238 },
5239 },
5240 "subdir2": {
5241 "file4.txt": ""
5242 }
5243 },
5244 "dir2": {
5245 "single_file": {
5246 "file5.txt": ""
5247 }
5248 }
5249 }),
5250 )
5251 .await;
5252
5253 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
5254 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5255 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5256
5257 // Test 1: Basic collapsing
5258 {
5259 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5260
5261 toggle_expand_dir(&panel, "root/dir1", cx);
5262 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
5263 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
5264 toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
5265
5266 assert_eq!(
5267 visible_entries_as_strings(&panel, 0..20, cx),
5268 &[
5269 "v root",
5270 " v dir1",
5271 " v subdir1",
5272 " v nested1",
5273 " file1.txt",
5274 " file2.txt",
5275 " v subdir2 <== selected",
5276 " file4.txt",
5277 " > dir2",
5278 ],
5279 "Initial state with everything expanded"
5280 );
5281
5282 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
5283 panel.update(cx, |panel, cx| {
5284 let project = panel.project.read(cx);
5285 let worktree = project.worktrees(cx).next().unwrap().read(cx);
5286 panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
5287 panel.update_visible_entries(None, cx);
5288 });
5289
5290 assert_eq!(
5291 visible_entries_as_strings(&panel, 0..20, cx),
5292 &["v root", " > dir1", " > dir2",],
5293 "All subdirs under dir1 should be collapsed"
5294 );
5295 }
5296
5297 // Test 2: With auto-fold enabled
5298 {
5299 cx.update(|_, cx| {
5300 let settings = *ProjectPanelSettings::get_global(cx);
5301 ProjectPanelSettings::override_global(
5302 ProjectPanelSettings {
5303 auto_fold_dirs: true,
5304 ..settings
5305 },
5306 cx,
5307 );
5308 });
5309
5310 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5311
5312 toggle_expand_dir(&panel, "root/dir1", cx);
5313 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
5314 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
5315
5316 assert_eq!(
5317 visible_entries_as_strings(&panel, 0..20, cx),
5318 &[
5319 "v root",
5320 " v dir1",
5321 " v subdir1/nested1 <== selected",
5322 " file1.txt",
5323 " file2.txt",
5324 " > subdir2",
5325 " > dir2/single_file",
5326 ],
5327 "Initial state with some dirs expanded"
5328 );
5329
5330 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
5331 panel.update(cx, |panel, cx| {
5332 let project = panel.project.read(cx);
5333 let worktree = project.worktrees(cx).next().unwrap().read(cx);
5334 panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
5335 });
5336
5337 toggle_expand_dir(&panel, "root/dir1", cx);
5338
5339 assert_eq!(
5340 visible_entries_as_strings(&panel, 0..20, cx),
5341 &[
5342 "v root",
5343 " v dir1 <== selected",
5344 " > subdir1/nested1",
5345 " > subdir2",
5346 " > dir2/single_file",
5347 ],
5348 "Subdirs should be collapsed and folded with auto-fold enabled"
5349 );
5350 }
5351
5352 // Test 3: With auto-fold disabled
5353 {
5354 cx.update(|_, cx| {
5355 let settings = *ProjectPanelSettings::get_global(cx);
5356 ProjectPanelSettings::override_global(
5357 ProjectPanelSettings {
5358 auto_fold_dirs: false,
5359 ..settings
5360 },
5361 cx,
5362 );
5363 });
5364
5365 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5366
5367 toggle_expand_dir(&panel, "root/dir1", cx);
5368 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
5369 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
5370
5371 assert_eq!(
5372 visible_entries_as_strings(&panel, 0..20, cx),
5373 &[
5374 "v root",
5375 " v dir1",
5376 " v subdir1",
5377 " v nested1 <== selected",
5378 " file1.txt",
5379 " file2.txt",
5380 " > subdir2",
5381 " > dir2",
5382 ],
5383 "Initial state with some dirs expanded and auto-fold disabled"
5384 );
5385
5386 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
5387 panel.update(cx, |panel, cx| {
5388 let project = panel.project.read(cx);
5389 let worktree = project.worktrees(cx).next().unwrap().read(cx);
5390 panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
5391 });
5392
5393 toggle_expand_dir(&panel, "root/dir1", cx);
5394
5395 assert_eq!(
5396 visible_entries_as_strings(&panel, 0..20, cx),
5397 &[
5398 "v root",
5399 " v dir1 <== selected",
5400 " > subdir1",
5401 " > subdir2",
5402 " > dir2",
5403 ],
5404 "Subdirs should be collapsed but not folded with auto-fold disabled"
5405 );
5406 }
5407}
5408
5409#[gpui::test]
5410async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) {
5411 init_test(cx);
5412
5413 let fs = FakeFs::new(cx.executor());
5414 fs.insert_tree(
5415 path!("/root"),
5416 json!({
5417 "dir1": {
5418 "file1.txt": "",
5419 },
5420 }),
5421 )
5422 .await;
5423
5424 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
5425 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5426 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5427
5428 let panel = workspace
5429 .update(cx, |workspace, window, cx| {
5430 let panel = ProjectPanel::new(workspace, window, cx);
5431 workspace.add_panel(panel.clone(), window, cx);
5432 panel
5433 })
5434 .unwrap();
5435
5436 #[rustfmt::skip]
5437 assert_eq!(
5438 visible_entries_as_strings(&panel, 0..20, cx),
5439 &[
5440 "v root",
5441 " > dir1",
5442 ],
5443 "Initial state with nothing selected"
5444 );
5445
5446 panel.update_in(cx, |panel, window, cx| {
5447 panel.new_file(&NewFile, window, cx);
5448 });
5449 panel.update_in(cx, |panel, window, cx| {
5450 assert!(panel.filename_editor.read(cx).is_focused(window));
5451 });
5452 panel
5453 .update_in(cx, |panel, window, cx| {
5454 panel.filename_editor.update(cx, |editor, cx| {
5455 editor.set_text("hello_from_no_selections", window, cx)
5456 });
5457 panel.confirm_edit(window, cx).unwrap()
5458 })
5459 .await
5460 .unwrap();
5461
5462 #[rustfmt::skip]
5463 assert_eq!(
5464 visible_entries_as_strings(&panel, 0..20, cx),
5465 &[
5466 "v root",
5467 " > dir1",
5468 " hello_from_no_selections <== selected <== marked",
5469 ],
5470 "A new file is created under the root directory"
5471 );
5472}
5473
5474#[gpui::test]
5475async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppContext) {
5476 init_test(cx);
5477
5478 let fs = FakeFs::new(cx.executor());
5479 fs.insert_tree(
5480 path!("/root"),
5481 json!({
5482 "existing_dir": {
5483 "existing_file.txt": "",
5484 },
5485 "existing_file.txt": "",
5486 }),
5487 )
5488 .await;
5489
5490 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
5491 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5492 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5493
5494 cx.update(|_, cx| {
5495 let settings = *ProjectPanelSettings::get_global(cx);
5496 ProjectPanelSettings::override_global(
5497 ProjectPanelSettings {
5498 hide_root: true,
5499 ..settings
5500 },
5501 cx,
5502 );
5503 });
5504
5505 let panel = workspace
5506 .update(cx, |workspace, window, cx| {
5507 let panel = ProjectPanel::new(workspace, window, cx);
5508 workspace.add_panel(panel.clone(), window, cx);
5509 panel
5510 })
5511 .unwrap();
5512
5513 #[rustfmt::skip]
5514 assert_eq!(
5515 visible_entries_as_strings(&panel, 0..20, cx),
5516 &[
5517 "> existing_dir",
5518 " existing_file.txt",
5519 ],
5520 "Initial state with hide_root=true, root should be hidden and nothing selected"
5521 );
5522
5523 panel.update(cx, |panel, _| {
5524 assert!(
5525 panel.selection.is_none(),
5526 "Should have no selection initially"
5527 );
5528 });
5529
5530 // Test 1: Create new file when no entry is selected
5531 panel.update_in(cx, |panel, window, cx| {
5532 panel.new_file(&NewFile, window, cx);
5533 });
5534 panel.update_in(cx, |panel, window, cx| {
5535 assert!(panel.filename_editor.read(cx).is_focused(window));
5536 });
5537
5538 #[rustfmt::skip]
5539 assert_eq!(
5540 visible_entries_as_strings(&panel, 0..20, cx),
5541 &[
5542 "> existing_dir",
5543 " [EDITOR: ''] <== selected",
5544 " existing_file.txt",
5545 ],
5546 "Editor should appear at root level when hide_root=true and no selection"
5547 );
5548
5549 let confirm = panel.update_in(cx, |panel, window, cx| {
5550 panel.filename_editor.update(cx, |editor, cx| {
5551 editor.set_text("new_file_at_root.txt", window, cx)
5552 });
5553 panel.confirm_edit(window, cx).unwrap()
5554 });
5555 confirm.await.unwrap();
5556
5557 #[rustfmt::skip]
5558 assert_eq!(
5559 visible_entries_as_strings(&panel, 0..20, cx),
5560 &[
5561 "> existing_dir",
5562 " existing_file.txt",
5563 " new_file_at_root.txt <== selected <== marked",
5564 ],
5565 "New file should be created at root level and visible without root prefix"
5566 );
5567
5568 assert!(
5569 fs.is_file(Path::new("/root/new_file_at_root.txt")).await,
5570 "File should be created in the actual root directory"
5571 );
5572
5573 // Test 2: Create new directory when no entry is selected
5574 panel.update(cx, |panel, _| {
5575 panel.selection = None;
5576 });
5577
5578 panel.update_in(cx, |panel, window, cx| {
5579 panel.new_directory(&NewDirectory, window, cx);
5580 });
5581 panel.update_in(cx, |panel, window, cx| {
5582 assert!(panel.filename_editor.read(cx).is_focused(window));
5583 });
5584
5585 #[rustfmt::skip]
5586 assert_eq!(
5587 visible_entries_as_strings(&panel, 0..20, cx),
5588 &[
5589 "> [EDITOR: ''] <== selected",
5590 "> existing_dir",
5591 " existing_file.txt",
5592 " new_file_at_root.txt",
5593 ],
5594 "Directory editor should appear at root level when hide_root=true and no selection"
5595 );
5596
5597 let confirm = panel.update_in(cx, |panel, window, cx| {
5598 panel.filename_editor.update(cx, |editor, cx| {
5599 editor.set_text("new_dir_at_root", window, cx)
5600 });
5601 panel.confirm_edit(window, cx).unwrap()
5602 });
5603 confirm.await.unwrap();
5604
5605 #[rustfmt::skip]
5606 assert_eq!(
5607 visible_entries_as_strings(&panel, 0..20, cx),
5608 &[
5609 "> existing_dir",
5610 "v new_dir_at_root <== selected",
5611 " existing_file.txt",
5612 " new_file_at_root.txt",
5613 ],
5614 "New directory should be created at root level and visible without root prefix"
5615 );
5616
5617 assert!(
5618 fs.is_dir(Path::new("/root/new_dir_at_root")).await,
5619 "Directory should be created in the actual root directory"
5620 );
5621}
5622
5623#[gpui::test]
5624async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) {
5625 init_test(cx);
5626
5627 let fs = FakeFs::new(cx.executor());
5628 fs.insert_tree(
5629 "/root",
5630 json!({
5631 "dir1": {
5632 "file1.txt": "",
5633 "dir2": {
5634 "file2.txt": ""
5635 }
5636 },
5637 "file3.txt": ""
5638 }),
5639 )
5640 .await;
5641
5642 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5643 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5644 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5645 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5646
5647 panel.update(cx, |panel, cx| {
5648 let project = panel.project.read(cx);
5649 let worktree = project.visible_worktrees(cx).next().unwrap();
5650 let worktree = worktree.read(cx);
5651
5652 // Test 1: Target is a directory, should highlight the directory itself
5653 let dir_entry = worktree.entry_for_path("dir1").unwrap();
5654 let result = panel.highlight_entry_for_external_drag(dir_entry, worktree);
5655 assert_eq!(
5656 result,
5657 Some(dir_entry.id),
5658 "Should highlight directory itself"
5659 );
5660
5661 // Test 2: Target is nested file, should highlight immediate parent
5662 let nested_file = worktree.entry_for_path("dir1/dir2/file2.txt").unwrap();
5663 let nested_parent = worktree.entry_for_path("dir1/dir2").unwrap();
5664 let result = panel.highlight_entry_for_external_drag(nested_file, worktree);
5665 assert_eq!(
5666 result,
5667 Some(nested_parent.id),
5668 "Should highlight immediate parent"
5669 );
5670
5671 // Test 3: Target is root level file, should highlight root
5672 let root_file = worktree.entry_for_path("file3.txt").unwrap();
5673 let result = panel.highlight_entry_for_external_drag(root_file, worktree);
5674 assert_eq!(
5675 result,
5676 Some(worktree.root_entry().unwrap().id),
5677 "Root level file should return None"
5678 );
5679
5680 // Test 4: Target is root itself, should highlight root
5681 let root_entry = worktree.root_entry().unwrap();
5682 let result = panel.highlight_entry_for_external_drag(root_entry, worktree);
5683 assert_eq!(
5684 result,
5685 Some(root_entry.id),
5686 "Root level file should return None"
5687 );
5688 });
5689}
5690
5691#[gpui::test]
5692async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) {
5693 init_test(cx);
5694
5695 let fs = FakeFs::new(cx.executor());
5696 fs.insert_tree(
5697 "/root",
5698 json!({
5699 "parent_dir": {
5700 "child_file.txt": "",
5701 "sibling_file.txt": "",
5702 "child_dir": {
5703 "nested_file.txt": ""
5704 }
5705 },
5706 "other_dir": {
5707 "other_file.txt": ""
5708 }
5709 }),
5710 )
5711 .await;
5712
5713 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5714 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5715 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5716 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5717
5718 panel.update(cx, |panel, cx| {
5719 let project = panel.project.read(cx);
5720 let worktree = project.visible_worktrees(cx).next().unwrap();
5721 let worktree_id = worktree.read(cx).id();
5722 let worktree = worktree.read(cx);
5723
5724 let parent_dir = worktree.entry_for_path("parent_dir").unwrap();
5725 let child_file = worktree
5726 .entry_for_path("parent_dir/child_file.txt")
5727 .unwrap();
5728 let sibling_file = worktree
5729 .entry_for_path("parent_dir/sibling_file.txt")
5730 .unwrap();
5731 let child_dir = worktree.entry_for_path("parent_dir/child_dir").unwrap();
5732 let other_dir = worktree.entry_for_path("other_dir").unwrap();
5733 let other_file = worktree.entry_for_path("other_dir/other_file.txt").unwrap();
5734
5735 // Test 1: Single item drag, don't highlight parent directory
5736 let dragged_selection = DraggedSelection {
5737 active_selection: SelectedEntry {
5738 worktree_id,
5739 entry_id: child_file.id,
5740 },
5741 marked_selections: Arc::new([SelectedEntry {
5742 worktree_id,
5743 entry_id: child_file.id,
5744 }]),
5745 };
5746 let result =
5747 panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
5748 assert_eq!(result, None, "Should not highlight parent of dragged item");
5749
5750 // Test 2: Single item drag, don't highlight sibling files
5751 let result = panel.highlight_entry_for_selection_drag(
5752 sibling_file,
5753 worktree,
5754 &dragged_selection,
5755 cx,
5756 );
5757 assert_eq!(result, None, "Should not highlight sibling files");
5758
5759 // Test 3: Single item drag, highlight unrelated directory
5760 let result =
5761 panel.highlight_entry_for_selection_drag(other_dir, worktree, &dragged_selection, cx);
5762 assert_eq!(
5763 result,
5764 Some(other_dir.id),
5765 "Should highlight unrelated directory"
5766 );
5767
5768 // Test 4: Single item drag, highlight sibling directory
5769 let result =
5770 panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
5771 assert_eq!(
5772 result,
5773 Some(child_dir.id),
5774 "Should highlight sibling directory"
5775 );
5776
5777 // Test 5: Multiple items drag, highlight parent directory
5778 let dragged_selection = DraggedSelection {
5779 active_selection: SelectedEntry {
5780 worktree_id,
5781 entry_id: child_file.id,
5782 },
5783 marked_selections: Arc::new([
5784 SelectedEntry {
5785 worktree_id,
5786 entry_id: child_file.id,
5787 },
5788 SelectedEntry {
5789 worktree_id,
5790 entry_id: sibling_file.id,
5791 },
5792 ]),
5793 };
5794 let result =
5795 panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
5796 assert_eq!(
5797 result,
5798 Some(parent_dir.id),
5799 "Should highlight parent with multiple items"
5800 );
5801
5802 // Test 6: Target is file in different directory, highlight parent
5803 let result =
5804 panel.highlight_entry_for_selection_drag(other_file, worktree, &dragged_selection, cx);
5805 assert_eq!(
5806 result,
5807 Some(other_dir.id),
5808 "Should highlight parent of target file"
5809 );
5810
5811 // Test 7: Target is directory, always highlight
5812 let result =
5813 panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
5814 assert_eq!(
5815 result,
5816 Some(child_dir.id),
5817 "Should always highlight directories"
5818 );
5819 });
5820}
5821
5822#[gpui::test]
5823async fn test_highlight_entry_for_selection_drag_cross_worktree(cx: &mut gpui::TestAppContext) {
5824 init_test(cx);
5825
5826 let fs = FakeFs::new(cx.executor());
5827 fs.insert_tree(
5828 "/root1",
5829 json!({
5830 "src": {
5831 "main.rs": "",
5832 "lib.rs": ""
5833 }
5834 }),
5835 )
5836 .await;
5837 fs.insert_tree(
5838 "/root2",
5839 json!({
5840 "src": {
5841 "main.rs": "",
5842 "test.rs": ""
5843 }
5844 }),
5845 )
5846 .await;
5847
5848 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
5849 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5850 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5851 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5852
5853 panel.update(cx, |panel, cx| {
5854 let project = panel.project.read(cx);
5855 let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
5856
5857 let worktree_a = &worktrees[0];
5858 let main_rs_from_a = worktree_a.read(cx).entry_for_path("src/main.rs").unwrap();
5859
5860 let worktree_b = &worktrees[1];
5861 let src_dir_from_b = worktree_b.read(cx).entry_for_path("src").unwrap();
5862 let main_rs_from_b = worktree_b.read(cx).entry_for_path("src/main.rs").unwrap();
5863
5864 // Test dragging file from worktree A onto parent of file with same relative path in worktree B
5865 let dragged_selection = DraggedSelection {
5866 active_selection: SelectedEntry {
5867 worktree_id: worktree_a.read(cx).id(),
5868 entry_id: main_rs_from_a.id,
5869 },
5870 marked_selections: Arc::new([SelectedEntry {
5871 worktree_id: worktree_a.read(cx).id(),
5872 entry_id: main_rs_from_a.id,
5873 }]),
5874 };
5875
5876 let result = panel.highlight_entry_for_selection_drag(
5877 src_dir_from_b,
5878 worktree_b.read(cx),
5879 &dragged_selection,
5880 cx,
5881 );
5882 assert_eq!(
5883 result,
5884 Some(src_dir_from_b.id),
5885 "Should highlight target directory from different worktree even with same relative path"
5886 );
5887
5888 // Test dragging file from worktree A onto file with same relative path in worktree B
5889 let result = panel.highlight_entry_for_selection_drag(
5890 main_rs_from_b,
5891 worktree_b.read(cx),
5892 &dragged_selection,
5893 cx,
5894 );
5895 assert_eq!(
5896 result,
5897 Some(src_dir_from_b.id),
5898 "Should highlight parent of target file from different worktree"
5899 );
5900 });
5901}
5902
5903#[gpui::test]
5904async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::TestAppContext) {
5905 init_test(cx);
5906
5907 let fs = FakeFs::new(cx.executor());
5908 fs.insert_tree(
5909 "/root1",
5910 json!({
5911 "parent_dir": {
5912 "child_file.txt": "",
5913 "nested_dir": {
5914 "nested_file.txt": ""
5915 }
5916 },
5917 "root_file.txt": ""
5918 }),
5919 )
5920 .await;
5921
5922 fs.insert_tree(
5923 "/root2",
5924 json!({
5925 "other_dir": {
5926 "other_file.txt": ""
5927 }
5928 }),
5929 )
5930 .await;
5931
5932 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
5933 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5934 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5935 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5936
5937 panel.update(cx, |panel, cx| {
5938 let project = panel.project.read(cx);
5939 let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
5940 let worktree1 = worktrees[0].read(cx);
5941 let worktree2 = worktrees[1].read(cx);
5942 let worktree1_id = worktree1.id();
5943 let _worktree2_id = worktree2.id();
5944
5945 let root1_entry = worktree1.root_entry().unwrap();
5946 let root2_entry = worktree2.root_entry().unwrap();
5947 let _parent_dir = worktree1.entry_for_path("parent_dir").unwrap();
5948 let child_file = worktree1
5949 .entry_for_path("parent_dir/child_file.txt")
5950 .unwrap();
5951 let nested_file = worktree1
5952 .entry_for_path("parent_dir/nested_dir/nested_file.txt")
5953 .unwrap();
5954 let root_file = worktree1.entry_for_path("root_file.txt").unwrap();
5955
5956 // Test 1: Multiple entries - should always highlight background
5957 let multiple_dragged_selection = DraggedSelection {
5958 active_selection: SelectedEntry {
5959 worktree_id: worktree1_id,
5960 entry_id: child_file.id,
5961 },
5962 marked_selections: Arc::new([
5963 SelectedEntry {
5964 worktree_id: worktree1_id,
5965 entry_id: child_file.id,
5966 },
5967 SelectedEntry {
5968 worktree_id: worktree1_id,
5969 entry_id: nested_file.id,
5970 },
5971 ]),
5972 };
5973
5974 let result = panel.should_highlight_background_for_selection_drag(
5975 &multiple_dragged_selection,
5976 root1_entry.id,
5977 cx,
5978 );
5979 assert!(result, "Should highlight background for multiple entries");
5980
5981 // Test 2: Single entry with non-empty parent path - should highlight background
5982 let nested_dragged_selection = DraggedSelection {
5983 active_selection: SelectedEntry {
5984 worktree_id: worktree1_id,
5985 entry_id: nested_file.id,
5986 },
5987 marked_selections: Arc::new([SelectedEntry {
5988 worktree_id: worktree1_id,
5989 entry_id: nested_file.id,
5990 }]),
5991 };
5992
5993 let result = panel.should_highlight_background_for_selection_drag(
5994 &nested_dragged_selection,
5995 root1_entry.id,
5996 cx,
5997 );
5998 assert!(result, "Should highlight background for nested file");
5999
6000 // Test 3: Single entry at root level, same worktree - should NOT highlight background
6001 let root_file_dragged_selection = DraggedSelection {
6002 active_selection: SelectedEntry {
6003 worktree_id: worktree1_id,
6004 entry_id: root_file.id,
6005 },
6006 marked_selections: Arc::new([SelectedEntry {
6007 worktree_id: worktree1_id,
6008 entry_id: root_file.id,
6009 }]),
6010 };
6011
6012 let result = panel.should_highlight_background_for_selection_drag(
6013 &root_file_dragged_selection,
6014 root1_entry.id,
6015 cx,
6016 );
6017 assert!(
6018 !result,
6019 "Should NOT highlight background for root file in same worktree"
6020 );
6021
6022 // Test 4: Single entry at root level, different worktree - should highlight background
6023 let result = panel.should_highlight_background_for_selection_drag(
6024 &root_file_dragged_selection,
6025 root2_entry.id,
6026 cx,
6027 );
6028 assert!(
6029 result,
6030 "Should highlight background for root file from different worktree"
6031 );
6032
6033 // Test 5: Single entry in subdirectory - should highlight background
6034 let child_file_dragged_selection = DraggedSelection {
6035 active_selection: SelectedEntry {
6036 worktree_id: worktree1_id,
6037 entry_id: child_file.id,
6038 },
6039 marked_selections: Arc::new([SelectedEntry {
6040 worktree_id: worktree1_id,
6041 entry_id: child_file.id,
6042 }]),
6043 };
6044
6045 let result = panel.should_highlight_background_for_selection_drag(
6046 &child_file_dragged_selection,
6047 root1_entry.id,
6048 cx,
6049 );
6050 assert!(
6051 result,
6052 "Should highlight background for file with non-empty parent path"
6053 );
6054 });
6055}
6056
6057#[gpui::test]
6058async fn test_hide_root(cx: &mut gpui::TestAppContext) {
6059 init_test(cx);
6060
6061 let fs = FakeFs::new(cx.executor());
6062 fs.insert_tree(
6063 "/root1",
6064 json!({
6065 "dir1": {
6066 "file1.txt": "content",
6067 "file2.txt": "content",
6068 },
6069 "dir2": {
6070 "file3.txt": "content",
6071 },
6072 "file4.txt": "content",
6073 }),
6074 )
6075 .await;
6076
6077 fs.insert_tree(
6078 "/root2",
6079 json!({
6080 "dir3": {
6081 "file5.txt": "content",
6082 },
6083 "file6.txt": "content",
6084 }),
6085 )
6086 .await;
6087
6088 // Test 1: Single worktree with hide_root = false
6089 {
6090 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
6091 let workspace =
6092 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6093 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6094
6095 cx.update(|_, cx| {
6096 let settings = *ProjectPanelSettings::get_global(cx);
6097 ProjectPanelSettings::override_global(
6098 ProjectPanelSettings {
6099 hide_root: false,
6100 ..settings
6101 },
6102 cx,
6103 );
6104 });
6105
6106 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6107
6108 #[rustfmt::skip]
6109 assert_eq!(
6110 visible_entries_as_strings(&panel, 0..10, cx),
6111 &[
6112 "v root1",
6113 " > dir1",
6114 " > dir2",
6115 " file4.txt",
6116 ],
6117 "With hide_root=false and single worktree, root should be visible"
6118 );
6119 }
6120
6121 // Test 2: Single worktree with hide_root = true
6122 {
6123 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
6124 let workspace =
6125 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6126 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6127
6128 // Set hide_root to true
6129 cx.update(|_, cx| {
6130 let settings = *ProjectPanelSettings::get_global(cx);
6131 ProjectPanelSettings::override_global(
6132 ProjectPanelSettings {
6133 hide_root: true,
6134 ..settings
6135 },
6136 cx,
6137 );
6138 });
6139
6140 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6141
6142 assert_eq!(
6143 visible_entries_as_strings(&panel, 0..10, cx),
6144 &["> dir1", "> dir2", " file4.txt",],
6145 "With hide_root=true and single worktree, root should be hidden"
6146 );
6147
6148 // Test expanding directories still works without root
6149 toggle_expand_dir(&panel, "root1/dir1", cx);
6150 assert_eq!(
6151 visible_entries_as_strings(&panel, 0..10, cx),
6152 &[
6153 "v dir1 <== selected",
6154 " file1.txt",
6155 " file2.txt",
6156 "> dir2",
6157 " file4.txt",
6158 ],
6159 "Should be able to expand directories even when root is hidden"
6160 );
6161 }
6162
6163 // Test 3: Multiple worktrees with hide_root = true
6164 {
6165 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
6166 let workspace =
6167 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6168 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6169
6170 // Set hide_root to true
6171 cx.update(|_, cx| {
6172 let settings = *ProjectPanelSettings::get_global(cx);
6173 ProjectPanelSettings::override_global(
6174 ProjectPanelSettings {
6175 hide_root: true,
6176 ..settings
6177 },
6178 cx,
6179 );
6180 });
6181
6182 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6183
6184 assert_eq!(
6185 visible_entries_as_strings(&panel, 0..10, cx),
6186 &[
6187 "v root1",
6188 " > dir1",
6189 " > dir2",
6190 " file4.txt",
6191 "v root2",
6192 " > dir3",
6193 " file6.txt",
6194 ],
6195 "With hide_root=true and multiple worktrees, roots should still be visible"
6196 );
6197 }
6198
6199 // Test 4: Multiple worktrees with hide_root = false
6200 {
6201 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
6202 let workspace =
6203 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6204 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6205
6206 cx.update(|_, cx| {
6207 let settings = *ProjectPanelSettings::get_global(cx);
6208 ProjectPanelSettings::override_global(
6209 ProjectPanelSettings {
6210 hide_root: false,
6211 ..settings
6212 },
6213 cx,
6214 );
6215 });
6216
6217 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6218
6219 assert_eq!(
6220 visible_entries_as_strings(&panel, 0..10, cx),
6221 &[
6222 "v root1",
6223 " > dir1",
6224 " > dir2",
6225 " file4.txt",
6226 "v root2",
6227 " > dir3",
6228 " file6.txt",
6229 ],
6230 "With hide_root=false and multiple worktrees, roots should be visible"
6231 );
6232 }
6233}
6234
6235#[gpui::test]
6236async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) {
6237 init_test_with_editor(cx);
6238
6239 let fs = FakeFs::new(cx.executor());
6240 fs.insert_tree(
6241 "/root",
6242 json!({
6243 "file1.txt": "content of file1",
6244 "file2.txt": "content of file2",
6245 "dir1": {
6246 "file3.txt": "content of file3"
6247 }
6248 }),
6249 )
6250 .await;
6251
6252 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
6253 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6254 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6255 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6256
6257 let file1_path = path!("root/file1.txt");
6258 let file2_path = path!("root/file2.txt");
6259 select_path_with_mark(&panel, file1_path, cx);
6260 select_path_with_mark(&panel, file2_path, cx);
6261
6262 panel.update_in(cx, |panel, window, cx| {
6263 panel.compare_marked_files(&CompareMarkedFiles, window, cx);
6264 });
6265 cx.executor().run_until_parked();
6266
6267 workspace
6268 .update(cx, |workspace, _, cx| {
6269 let active_items = workspace
6270 .panes()
6271 .iter()
6272 .filter_map(|pane| pane.read(cx).active_item())
6273 .collect::<Vec<_>>();
6274 assert_eq!(active_items.len(), 1);
6275 let diff_view = active_items
6276 .into_iter()
6277 .next()
6278 .unwrap()
6279 .downcast::<FileDiffView>()
6280 .expect("Open item should be an FileDiffView");
6281 assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt");
6282 assert_eq!(
6283 diff_view.tab_tooltip_text(cx).unwrap(),
6284 format!("{} ↔ {}", file1_path, file2_path)
6285 );
6286 })
6287 .unwrap();
6288
6289 let file1_entry_id = find_project_entry(&panel, file1_path, cx).unwrap();
6290 let file2_entry_id = find_project_entry(&panel, file2_path, cx).unwrap();
6291 let worktree_id = panel.update(cx, |panel, cx| {
6292 panel
6293 .project
6294 .read(cx)
6295 .worktrees(cx)
6296 .next()
6297 .unwrap()
6298 .read(cx)
6299 .id()
6300 });
6301
6302 let expected_entries = [
6303 SelectedEntry {
6304 worktree_id,
6305 entry_id: file1_entry_id,
6306 },
6307 SelectedEntry {
6308 worktree_id,
6309 entry_id: file2_entry_id,
6310 },
6311 ];
6312 panel.update(cx, |panel, _cx| {
6313 assert_eq!(
6314 &panel.marked_entries, &expected_entries,
6315 "Should keep marked entries after comparison"
6316 );
6317 });
6318
6319 panel.update(cx, |panel, cx| {
6320 panel.project.update(cx, |_, cx| {
6321 cx.emit(project::Event::RevealInProjectPanel(file2_entry_id))
6322 })
6323 });
6324
6325 panel.update(cx, |panel, _cx| {
6326 assert_eq!(
6327 &panel.marked_entries, &expected_entries,
6328 "Marked entries should persist after focusing back on the project panel"
6329 );
6330 });
6331}
6332
6333#[gpui::test]
6334async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) {
6335 init_test_with_editor(cx);
6336
6337 let fs = FakeFs::new(cx.executor());
6338 fs.insert_tree(
6339 "/root",
6340 json!({
6341 "file1.txt": "content of file1",
6342 "file2.txt": "content of file2",
6343 "dir1": {},
6344 "dir2": {
6345 "file3.txt": "content of file3"
6346 }
6347 }),
6348 )
6349 .await;
6350
6351 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
6352 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6353 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6354 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6355
6356 // Test 1: When only one file is selected, there should be no compare option
6357 select_path(&panel, "root/file1.txt", cx);
6358
6359 let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
6360 assert_eq!(
6361 selected_files, None,
6362 "Should not have compare option when only one file is selected"
6363 );
6364
6365 // Test 2: When multiple files are selected, there should be a compare option
6366 select_path_with_mark(&panel, "root/file1.txt", cx);
6367 select_path_with_mark(&panel, "root/file2.txt", cx);
6368
6369 let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
6370 assert!(
6371 selected_files.is_some(),
6372 "Should have files selected for comparison"
6373 );
6374 if let Some((file1, file2)) = selected_files {
6375 assert!(
6376 file1.to_string_lossy().ends_with("file1.txt")
6377 && file2.to_string_lossy().ends_with("file2.txt"),
6378 "Should have file1.txt and file2.txt as the selected files when multi-selecting"
6379 );
6380 }
6381
6382 // Test 3: Selecting a directory shouldn't count as a comparable file
6383 select_path_with_mark(&panel, "root/dir1", cx);
6384
6385 let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
6386 assert!(
6387 selected_files.is_some(),
6388 "Directory selection should not affect comparable files"
6389 );
6390 if let Some((file1, file2)) = selected_files {
6391 assert!(
6392 file1.to_string_lossy().ends_with("file1.txt")
6393 && file2.to_string_lossy().ends_with("file2.txt"),
6394 "Selecting a directory should not affect the number of comparable files"
6395 );
6396 }
6397
6398 // Test 4: Selecting one more file
6399 select_path_with_mark(&panel, "root/dir2/file3.txt", cx);
6400
6401 let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
6402 assert!(
6403 selected_files.is_some(),
6404 "Directory selection should not affect comparable files"
6405 );
6406 if let Some((file1, file2)) = selected_files {
6407 assert!(
6408 file1.to_string_lossy().ends_with("file2.txt")
6409 && file2.to_string_lossy().ends_with("file3.txt"),
6410 "Selecting a directory should not affect the number of comparable files"
6411 );
6412 }
6413}
6414
6415fn select_path(panel: &Entity<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
6416 let path = path.as_ref();
6417 panel.update(cx, |panel, cx| {
6418 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
6419 let worktree = worktree.read(cx);
6420 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
6421 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
6422 panel.selection = Some(crate::SelectedEntry {
6423 worktree_id: worktree.id(),
6424 entry_id,
6425 });
6426 return;
6427 }
6428 }
6429 panic!("no worktree for path {:?}", path);
6430 });
6431}
6432
6433fn select_path_with_mark(
6434 panel: &Entity<ProjectPanel>,
6435 path: impl AsRef<Path>,
6436 cx: &mut VisualTestContext,
6437) {
6438 let path = path.as_ref();
6439 panel.update(cx, |panel, cx| {
6440 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
6441 let worktree = worktree.read(cx);
6442 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
6443 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
6444 let entry = crate::SelectedEntry {
6445 worktree_id: worktree.id(),
6446 entry_id,
6447 };
6448 if !panel.marked_entries.contains(&entry) {
6449 panel.marked_entries.push(entry);
6450 }
6451 panel.selection = Some(entry);
6452 return;
6453 }
6454 }
6455 panic!("no worktree for path {:?}", path);
6456 });
6457}
6458
6459fn find_project_entry(
6460 panel: &Entity<ProjectPanel>,
6461 path: impl AsRef<Path>,
6462 cx: &mut VisualTestContext,
6463) -> Option<ProjectEntryId> {
6464 let path = path.as_ref();
6465 panel.update(cx, |panel, cx| {
6466 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
6467 let worktree = worktree.read(cx);
6468 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
6469 return worktree.entry_for_path(relative_path).map(|entry| entry.id);
6470 }
6471 }
6472 panic!("no worktree for path {path:?}");
6473 })
6474}
6475
6476fn visible_entries_as_strings(
6477 panel: &Entity<ProjectPanel>,
6478 range: Range<usize>,
6479 cx: &mut VisualTestContext,
6480) -> Vec<String> {
6481 let mut result = Vec::new();
6482 let mut project_entries = HashSet::default();
6483 let mut has_editor = false;
6484
6485 panel.update_in(cx, |panel, window, cx| {
6486 panel.for_each_visible_entry(range, window, cx, |project_entry, details, _, _| {
6487 if details.is_editing {
6488 assert!(!has_editor, "duplicate editor entry");
6489 has_editor = true;
6490 } else {
6491 assert!(
6492 project_entries.insert(project_entry),
6493 "duplicate project entry {:?} {:?}",
6494 project_entry,
6495 details
6496 );
6497 }
6498
6499 let indent = " ".repeat(details.depth);
6500 let icon = if details.kind.is_dir() {
6501 if details.is_expanded { "v " } else { "> " }
6502 } else {
6503 " "
6504 };
6505 #[cfg(windows)]
6506 let filename = details.filename.replace("\\", "/");
6507 #[cfg(not(windows))]
6508 let filename = details.filename;
6509 let name = if details.is_editing {
6510 format!("[EDITOR: '{}']", filename)
6511 } else if details.is_processing {
6512 format!("[PROCESSING: '{}']", filename)
6513 } else {
6514 filename
6515 };
6516 let selected = if details.is_selected {
6517 " <== selected"
6518 } else {
6519 ""
6520 };
6521 let marked = if details.is_marked {
6522 " <== marked"
6523 } else {
6524 ""
6525 };
6526
6527 result.push(format!("{indent}{icon}{name}{selected}{marked}"));
6528 });
6529 });
6530
6531 result
6532}
6533
6534fn init_test(cx: &mut TestAppContext) {
6535 cx.update(|cx| {
6536 let settings_store = SettingsStore::test(cx);
6537 cx.set_global(settings_store);
6538 init_settings(cx);
6539 theme::init(theme::LoadThemes::JustBase, cx);
6540 language::init(cx);
6541 editor::init_settings(cx);
6542 crate::init(cx);
6543 workspace::init_settings(cx);
6544 client::init_settings(cx);
6545 Project::init_settings(cx);
6546
6547 cx.update_global::<SettingsStore, _>(|store, cx| {
6548 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
6549 project_panel_settings.auto_fold_dirs = Some(false);
6550 });
6551 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
6552 worktree_settings.file_scan_exclusions = Some(Vec::new());
6553 });
6554 });
6555 });
6556}
6557
6558fn init_test_with_editor(cx: &mut TestAppContext) {
6559 cx.update(|cx| {
6560 let app_state = AppState::test(cx);
6561 theme::init(theme::LoadThemes::JustBase, cx);
6562 init_settings(cx);
6563 language::init(cx);
6564 editor::init(cx);
6565 crate::init(cx);
6566 workspace::init(app_state, cx);
6567 Project::init_settings(cx);
6568
6569 cx.update_global::<SettingsStore, _>(|store, cx| {
6570 store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
6571 project_panel_settings.auto_fold_dirs = Some(false);
6572 });
6573 store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
6574 worktree_settings.file_scan_exclusions = Some(Vec::new());
6575 });
6576 });
6577 });
6578}
6579
6580fn ensure_single_file_is_opened(
6581 window: &WindowHandle<Workspace>,
6582 expected_path: &str,
6583 cx: &mut TestAppContext,
6584) {
6585 window
6586 .update(cx, |workspace, _, cx| {
6587 let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
6588 assert_eq!(worktrees.len(), 1);
6589 let worktree_id = worktrees[0].read(cx).id();
6590
6591 let open_project_paths = workspace
6592 .panes()
6593 .iter()
6594 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
6595 .collect::<Vec<_>>();
6596 assert_eq!(
6597 open_project_paths,
6598 vec![ProjectPath {
6599 worktree_id,
6600 path: Arc::from(Path::new(expected_path))
6601 }],
6602 "Should have opened file, selected in project panel"
6603 );
6604 })
6605 .unwrap();
6606}
6607
6608fn submit_deletion(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
6609 assert!(
6610 !cx.has_pending_prompt(),
6611 "Should have no prompts before the deletion"
6612 );
6613 panel.update_in(cx, |panel, window, cx| {
6614 panel.delete(&Delete { skip_prompt: false }, window, cx)
6615 });
6616 assert!(
6617 cx.has_pending_prompt(),
6618 "Should have a prompt after the deletion"
6619 );
6620 cx.simulate_prompt_answer("Delete");
6621 assert!(
6622 !cx.has_pending_prompt(),
6623 "Should have no prompts after prompt was replied to"
6624 );
6625 cx.executor().run_until_parked();
6626}
6627
6628fn submit_deletion_skipping_prompt(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
6629 assert!(
6630 !cx.has_pending_prompt(),
6631 "Should have no prompts before the deletion"
6632 );
6633 panel.update_in(cx, |panel, window, cx| {
6634 panel.delete(&Delete { skip_prompt: true }, window, cx)
6635 });
6636 assert!(!cx.has_pending_prompt(), "Should have received no prompts");
6637 cx.executor().run_until_parked();
6638}
6639
6640fn ensure_no_open_items_and_panes(workspace: &WindowHandle<Workspace>, cx: &mut VisualTestContext) {
6641 assert!(
6642 !cx.has_pending_prompt(),
6643 "Should have no prompts after deletion operation closes the file"
6644 );
6645 workspace
6646 .read_with(cx, |workspace, cx| {
6647 let open_project_paths = workspace
6648 .panes()
6649 .iter()
6650 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
6651 .collect::<Vec<_>>();
6652 assert!(
6653 open_project_paths.is_empty(),
6654 "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
6655 );
6656 })
6657 .unwrap();
6658}
6659
6660struct TestProjectItemView {
6661 focus_handle: FocusHandle,
6662 path: ProjectPath,
6663}
6664
6665struct TestProjectItem {
6666 path: ProjectPath,
6667}
6668
6669impl project::ProjectItem for TestProjectItem {
6670 fn try_open(
6671 _project: &Entity<Project>,
6672 path: &ProjectPath,
6673 cx: &mut App,
6674 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
6675 let path = path.clone();
6676 Some(cx.spawn(async move |cx| cx.new(|_| Self { path })))
6677 }
6678
6679 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
6680 None
6681 }
6682
6683 fn project_path(&self, _: &App) -> Option<ProjectPath> {
6684 Some(self.path.clone())
6685 }
6686
6687 fn is_dirty(&self) -> bool {
6688 false
6689 }
6690}
6691
6692impl ProjectItem for TestProjectItemView {
6693 type Item = TestProjectItem;
6694
6695 fn for_project_item(
6696 _: Entity<Project>,
6697 _: Option<&Pane>,
6698 project_item: Entity<Self::Item>,
6699 _: &mut Window,
6700 cx: &mut Context<Self>,
6701 ) -> Self
6702 where
6703 Self: Sized,
6704 {
6705 Self {
6706 path: project_item.update(cx, |project_item, _| project_item.path.clone()),
6707 focus_handle: cx.focus_handle(),
6708 }
6709 }
6710}
6711
6712impl Item for TestProjectItemView {
6713 type Event = ();
6714
6715 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
6716 "Test".into()
6717 }
6718}
6719
6720impl EventEmitter<()> for TestProjectItemView {}
6721
6722impl Focusable for TestProjectItemView {
6723 fn focus_handle(&self, _: &App) -> FocusHandle {
6724 self.focus_handle.clone()
6725 }
6726}
6727
6728impl Render for TestProjectItemView {
6729 fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
6730 Empty
6731 }
6732}