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