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