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#[gpui::test]
6666async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) {
6667 init_test(cx);
6668
6669 let fs = FakeFs::new(cx.executor());
6670 fs.insert_tree(
6671 "/root",
6672 json!({
6673 "dir1": {
6674 "file1.txt": "",
6675 "dir2": {
6676 "file2.txt": ""
6677 }
6678 },
6679 "file3.txt": ""
6680 }),
6681 )
6682 .await;
6683
6684 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
6685 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6686 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6687 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6688 cx.run_until_parked();
6689
6690 panel.update(cx, |panel, cx| {
6691 let project = panel.project.read(cx);
6692 let worktree = project.visible_worktrees(cx).next().unwrap();
6693 let worktree = worktree.read(cx);
6694
6695 // Test 1: Target is a directory, should highlight the directory itself
6696 let dir_entry = worktree.entry_for_path(rel_path("dir1")).unwrap();
6697 let result = panel.highlight_entry_for_external_drag(dir_entry, worktree);
6698 assert_eq!(
6699 result,
6700 Some(dir_entry.id),
6701 "Should highlight directory itself"
6702 );
6703
6704 // Test 2: Target is nested file, should highlight immediate parent
6705 let nested_file = worktree
6706 .entry_for_path(rel_path("dir1/dir2/file2.txt"))
6707 .unwrap();
6708 let nested_parent = worktree.entry_for_path(rel_path("dir1/dir2")).unwrap();
6709 let result = panel.highlight_entry_for_external_drag(nested_file, worktree);
6710 assert_eq!(
6711 result,
6712 Some(nested_parent.id),
6713 "Should highlight immediate parent"
6714 );
6715
6716 // Test 3: Target is root level file, should highlight root
6717 let root_file = worktree.entry_for_path(rel_path("file3.txt")).unwrap();
6718 let result = panel.highlight_entry_for_external_drag(root_file, worktree);
6719 assert_eq!(
6720 result,
6721 Some(worktree.root_entry().unwrap().id),
6722 "Root level file should return None"
6723 );
6724
6725 // Test 4: Target is root itself, should highlight root
6726 let root_entry = worktree.root_entry().unwrap();
6727 let result = panel.highlight_entry_for_external_drag(root_entry, worktree);
6728 assert_eq!(
6729 result,
6730 Some(root_entry.id),
6731 "Root level file should return None"
6732 );
6733 });
6734}
6735
6736#[gpui::test]
6737async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) {
6738 init_test(cx);
6739
6740 let fs = FakeFs::new(cx.executor());
6741 fs.insert_tree(
6742 "/root",
6743 json!({
6744 "parent_dir": {
6745 "child_file.txt": "",
6746 "sibling_file.txt": "",
6747 "child_dir": {
6748 "nested_file.txt": ""
6749 }
6750 },
6751 "other_dir": {
6752 "other_file.txt": ""
6753 }
6754 }),
6755 )
6756 .await;
6757
6758 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
6759 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6760 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6761 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6762 cx.run_until_parked();
6763
6764 panel.update(cx, |panel, cx| {
6765 let project = panel.project.read(cx);
6766 let worktree = project.visible_worktrees(cx).next().unwrap();
6767 let worktree_id = worktree.read(cx).id();
6768 let worktree = worktree.read(cx);
6769
6770 let parent_dir = worktree.entry_for_path(rel_path("parent_dir")).unwrap();
6771 let child_file = worktree
6772 .entry_for_path(rel_path("parent_dir/child_file.txt"))
6773 .unwrap();
6774 let sibling_file = worktree
6775 .entry_for_path(rel_path("parent_dir/sibling_file.txt"))
6776 .unwrap();
6777 let child_dir = worktree
6778 .entry_for_path(rel_path("parent_dir/child_dir"))
6779 .unwrap();
6780 let other_dir = worktree.entry_for_path(rel_path("other_dir")).unwrap();
6781 let other_file = worktree
6782 .entry_for_path(rel_path("other_dir/other_file.txt"))
6783 .unwrap();
6784
6785 // Test 1: Single item drag, don't highlight parent directory
6786 let dragged_selection = DraggedSelection {
6787 active_selection: SelectedEntry {
6788 worktree_id,
6789 entry_id: child_file.id,
6790 },
6791 marked_selections: Arc::new([SelectedEntry {
6792 worktree_id,
6793 entry_id: child_file.id,
6794 }]),
6795 };
6796 let result =
6797 panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
6798 assert_eq!(result, None, "Should not highlight parent of dragged item");
6799
6800 // Test 2: Single item drag, don't highlight sibling files
6801 let result = panel.highlight_entry_for_selection_drag(
6802 sibling_file,
6803 worktree,
6804 &dragged_selection,
6805 cx,
6806 );
6807 assert_eq!(result, None, "Should not highlight sibling files");
6808
6809 // Test 3: Single item drag, highlight unrelated directory
6810 let result =
6811 panel.highlight_entry_for_selection_drag(other_dir, worktree, &dragged_selection, cx);
6812 assert_eq!(
6813 result,
6814 Some(other_dir.id),
6815 "Should highlight unrelated directory"
6816 );
6817
6818 // Test 4: Single item drag, highlight sibling directory
6819 let result =
6820 panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
6821 assert_eq!(
6822 result,
6823 Some(child_dir.id),
6824 "Should highlight sibling directory"
6825 );
6826
6827 // Test 5: Multiple items drag, highlight parent directory
6828 let dragged_selection = DraggedSelection {
6829 active_selection: SelectedEntry {
6830 worktree_id,
6831 entry_id: child_file.id,
6832 },
6833 marked_selections: Arc::new([
6834 SelectedEntry {
6835 worktree_id,
6836 entry_id: child_file.id,
6837 },
6838 SelectedEntry {
6839 worktree_id,
6840 entry_id: sibling_file.id,
6841 },
6842 ]),
6843 };
6844 let result =
6845 panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
6846 assert_eq!(
6847 result,
6848 Some(parent_dir.id),
6849 "Should highlight parent with multiple items"
6850 );
6851
6852 // Test 6: Target is file in different directory, highlight parent
6853 let result =
6854 panel.highlight_entry_for_selection_drag(other_file, worktree, &dragged_selection, cx);
6855 assert_eq!(
6856 result,
6857 Some(other_dir.id),
6858 "Should highlight parent of target file"
6859 );
6860
6861 // Test 7: Target is directory, always highlight
6862 let result =
6863 panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
6864 assert_eq!(
6865 result,
6866 Some(child_dir.id),
6867 "Should always highlight directories"
6868 );
6869 });
6870}
6871
6872#[gpui::test]
6873async fn test_highlight_entry_for_selection_drag_cross_worktree(cx: &mut gpui::TestAppContext) {
6874 init_test(cx);
6875
6876 let fs = FakeFs::new(cx.executor());
6877 fs.insert_tree(
6878 "/root1",
6879 json!({
6880 "src": {
6881 "main.rs": "",
6882 "lib.rs": ""
6883 }
6884 }),
6885 )
6886 .await;
6887 fs.insert_tree(
6888 "/root2",
6889 json!({
6890 "src": {
6891 "main.rs": "",
6892 "test.rs": ""
6893 }
6894 }),
6895 )
6896 .await;
6897
6898 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
6899 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6900 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6901 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6902 cx.run_until_parked();
6903
6904 panel.update(cx, |panel, cx| {
6905 let project = panel.project.read(cx);
6906 let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
6907
6908 let worktree_a = &worktrees[0];
6909 let main_rs_from_a = worktree_a
6910 .read(cx)
6911 .entry_for_path(rel_path("src/main.rs"))
6912 .unwrap();
6913
6914 let worktree_b = &worktrees[1];
6915 let src_dir_from_b = worktree_b.read(cx).entry_for_path(rel_path("src")).unwrap();
6916 let main_rs_from_b = worktree_b
6917 .read(cx)
6918 .entry_for_path(rel_path("src/main.rs"))
6919 .unwrap();
6920
6921 // Test dragging file from worktree A onto parent of file with same relative path in worktree B
6922 let dragged_selection = DraggedSelection {
6923 active_selection: SelectedEntry {
6924 worktree_id: worktree_a.read(cx).id(),
6925 entry_id: main_rs_from_a.id,
6926 },
6927 marked_selections: Arc::new([SelectedEntry {
6928 worktree_id: worktree_a.read(cx).id(),
6929 entry_id: main_rs_from_a.id,
6930 }]),
6931 };
6932
6933 let result = panel.highlight_entry_for_selection_drag(
6934 src_dir_from_b,
6935 worktree_b.read(cx),
6936 &dragged_selection,
6937 cx,
6938 );
6939 assert_eq!(
6940 result,
6941 Some(src_dir_from_b.id),
6942 "Should highlight target directory from different worktree even with same relative path"
6943 );
6944
6945 // Test dragging file from worktree A onto file with same relative path in worktree B
6946 let result = panel.highlight_entry_for_selection_drag(
6947 main_rs_from_b,
6948 worktree_b.read(cx),
6949 &dragged_selection,
6950 cx,
6951 );
6952 assert_eq!(
6953 result,
6954 Some(src_dir_from_b.id),
6955 "Should highlight parent of target file from different worktree"
6956 );
6957 });
6958}
6959
6960#[gpui::test]
6961async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::TestAppContext) {
6962 init_test(cx);
6963
6964 let fs = FakeFs::new(cx.executor());
6965 fs.insert_tree(
6966 "/root1",
6967 json!({
6968 "parent_dir": {
6969 "child_file.txt": "",
6970 "nested_dir": {
6971 "nested_file.txt": ""
6972 }
6973 },
6974 "root_file.txt": ""
6975 }),
6976 )
6977 .await;
6978
6979 fs.insert_tree(
6980 "/root2",
6981 json!({
6982 "other_dir": {
6983 "other_file.txt": ""
6984 }
6985 }),
6986 )
6987 .await;
6988
6989 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
6990 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6991 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6992 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6993 cx.run_until_parked();
6994
6995 panel.update(cx, |panel, cx| {
6996 let project = panel.project.read(cx);
6997 let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
6998 let worktree1 = worktrees[0].read(cx);
6999 let worktree2 = worktrees[1].read(cx);
7000 let worktree1_id = worktree1.id();
7001 let _worktree2_id = worktree2.id();
7002
7003 let root1_entry = worktree1.root_entry().unwrap();
7004 let root2_entry = worktree2.root_entry().unwrap();
7005 let _parent_dir = worktree1.entry_for_path(rel_path("parent_dir")).unwrap();
7006 let child_file = worktree1
7007 .entry_for_path(rel_path("parent_dir/child_file.txt"))
7008 .unwrap();
7009 let nested_file = worktree1
7010 .entry_for_path(rel_path("parent_dir/nested_dir/nested_file.txt"))
7011 .unwrap();
7012 let root_file = worktree1.entry_for_path(rel_path("root_file.txt")).unwrap();
7013
7014 // Test 1: Multiple entries - should always highlight background
7015 let multiple_dragged_selection = DraggedSelection {
7016 active_selection: SelectedEntry {
7017 worktree_id: worktree1_id,
7018 entry_id: child_file.id,
7019 },
7020 marked_selections: Arc::new([
7021 SelectedEntry {
7022 worktree_id: worktree1_id,
7023 entry_id: child_file.id,
7024 },
7025 SelectedEntry {
7026 worktree_id: worktree1_id,
7027 entry_id: nested_file.id,
7028 },
7029 ]),
7030 };
7031
7032 let result = panel.should_highlight_background_for_selection_drag(
7033 &multiple_dragged_selection,
7034 root1_entry.id,
7035 cx,
7036 );
7037 assert!(result, "Should highlight background for multiple entries");
7038
7039 // Test 2: Single entry with non-empty parent path - should highlight background
7040 let nested_dragged_selection = DraggedSelection {
7041 active_selection: SelectedEntry {
7042 worktree_id: worktree1_id,
7043 entry_id: nested_file.id,
7044 },
7045 marked_selections: Arc::new([SelectedEntry {
7046 worktree_id: worktree1_id,
7047 entry_id: nested_file.id,
7048 }]),
7049 };
7050
7051 let result = panel.should_highlight_background_for_selection_drag(
7052 &nested_dragged_selection,
7053 root1_entry.id,
7054 cx,
7055 );
7056 assert!(result, "Should highlight background for nested file");
7057
7058 // Test 3: Single entry at root level, same worktree - should NOT highlight background
7059 let root_file_dragged_selection = DraggedSelection {
7060 active_selection: SelectedEntry {
7061 worktree_id: worktree1_id,
7062 entry_id: root_file.id,
7063 },
7064 marked_selections: Arc::new([SelectedEntry {
7065 worktree_id: worktree1_id,
7066 entry_id: root_file.id,
7067 }]),
7068 };
7069
7070 let result = panel.should_highlight_background_for_selection_drag(
7071 &root_file_dragged_selection,
7072 root1_entry.id,
7073 cx,
7074 );
7075 assert!(
7076 !result,
7077 "Should NOT highlight background for root file in same worktree"
7078 );
7079
7080 // Test 4: Single entry at root level, different worktree - should highlight background
7081 let result = panel.should_highlight_background_for_selection_drag(
7082 &root_file_dragged_selection,
7083 root2_entry.id,
7084 cx,
7085 );
7086 assert!(
7087 result,
7088 "Should highlight background for root file from different worktree"
7089 );
7090
7091 // Test 5: Single entry in subdirectory - should highlight background
7092 let child_file_dragged_selection = DraggedSelection {
7093 active_selection: SelectedEntry {
7094 worktree_id: worktree1_id,
7095 entry_id: child_file.id,
7096 },
7097 marked_selections: Arc::new([SelectedEntry {
7098 worktree_id: worktree1_id,
7099 entry_id: child_file.id,
7100 }]),
7101 };
7102
7103 let result = panel.should_highlight_background_for_selection_drag(
7104 &child_file_dragged_selection,
7105 root1_entry.id,
7106 cx,
7107 );
7108 assert!(
7109 result,
7110 "Should highlight background for file with non-empty parent path"
7111 );
7112 });
7113}
7114
7115#[gpui::test]
7116async fn test_hide_root(cx: &mut gpui::TestAppContext) {
7117 init_test(cx);
7118
7119 let fs = FakeFs::new(cx.executor());
7120 fs.insert_tree(
7121 "/root1",
7122 json!({
7123 "dir1": {
7124 "file1.txt": "content",
7125 "file2.txt": "content",
7126 },
7127 "dir2": {
7128 "file3.txt": "content",
7129 },
7130 "file4.txt": "content",
7131 }),
7132 )
7133 .await;
7134
7135 fs.insert_tree(
7136 "/root2",
7137 json!({
7138 "dir3": {
7139 "file5.txt": "content",
7140 },
7141 "file6.txt": "content",
7142 }),
7143 )
7144 .await;
7145
7146 // Test 1: Single worktree with hide_root = false
7147 {
7148 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
7149 let workspace =
7150 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7151 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7152
7153 cx.update(|_, cx| {
7154 let settings = *ProjectPanelSettings::get_global(cx);
7155 ProjectPanelSettings::override_global(
7156 ProjectPanelSettings {
7157 hide_root: false,
7158 ..settings
7159 },
7160 cx,
7161 );
7162 });
7163
7164 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7165 cx.run_until_parked();
7166
7167 #[rustfmt::skip]
7168 assert_eq!(
7169 visible_entries_as_strings(&panel, 0..10, cx),
7170 &[
7171 "v root1",
7172 " > dir1",
7173 " > dir2",
7174 " file4.txt",
7175 ],
7176 "With hide_root=false and single worktree, root should be visible"
7177 );
7178 }
7179
7180 // Test 2: Single worktree with hide_root = true
7181 {
7182 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
7183 let workspace =
7184 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7185 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7186
7187 // Set hide_root to true
7188 cx.update(|_, cx| {
7189 let settings = *ProjectPanelSettings::get_global(cx);
7190 ProjectPanelSettings::override_global(
7191 ProjectPanelSettings {
7192 hide_root: true,
7193 ..settings
7194 },
7195 cx,
7196 );
7197 });
7198
7199 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7200 cx.run_until_parked();
7201
7202 assert_eq!(
7203 visible_entries_as_strings(&panel, 0..10, cx),
7204 &["> dir1", "> dir2", " file4.txt",],
7205 "With hide_root=true and single worktree, root should be hidden"
7206 );
7207
7208 // Test expanding directories still works without root
7209 toggle_expand_dir(&panel, "root1/dir1", cx);
7210 assert_eq!(
7211 visible_entries_as_strings(&panel, 0..10, cx),
7212 &[
7213 "v dir1 <== selected",
7214 " file1.txt",
7215 " file2.txt",
7216 "> dir2",
7217 " file4.txt",
7218 ],
7219 "Should be able to expand directories even when root is hidden"
7220 );
7221 }
7222
7223 // Test 3: Multiple worktrees with hide_root = true
7224 {
7225 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
7226 let workspace =
7227 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7228 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7229
7230 // Set hide_root to true
7231 cx.update(|_, cx| {
7232 let settings = *ProjectPanelSettings::get_global(cx);
7233 ProjectPanelSettings::override_global(
7234 ProjectPanelSettings {
7235 hide_root: true,
7236 ..settings
7237 },
7238 cx,
7239 );
7240 });
7241
7242 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7243 cx.run_until_parked();
7244
7245 assert_eq!(
7246 visible_entries_as_strings(&panel, 0..10, cx),
7247 &[
7248 "v root1",
7249 " > dir1",
7250 " > dir2",
7251 " file4.txt",
7252 "v root2",
7253 " > dir3",
7254 " file6.txt",
7255 ],
7256 "With hide_root=true and multiple worktrees, roots should still be visible"
7257 );
7258 }
7259
7260 // Test 4: Multiple worktrees with hide_root = false
7261 {
7262 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
7263 let workspace =
7264 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7265 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7266
7267 cx.update(|_, cx| {
7268 let settings = *ProjectPanelSettings::get_global(cx);
7269 ProjectPanelSettings::override_global(
7270 ProjectPanelSettings {
7271 hide_root: false,
7272 ..settings
7273 },
7274 cx,
7275 );
7276 });
7277
7278 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7279 cx.run_until_parked();
7280
7281 assert_eq!(
7282 visible_entries_as_strings(&panel, 0..10, cx),
7283 &[
7284 "v root1",
7285 " > dir1",
7286 " > dir2",
7287 " file4.txt",
7288 "v root2",
7289 " > dir3",
7290 " file6.txt",
7291 ],
7292 "With hide_root=false and multiple worktrees, roots should be visible"
7293 );
7294 }
7295}
7296
7297#[gpui::test]
7298async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) {
7299 init_test_with_editor(cx);
7300
7301 let fs = FakeFs::new(cx.executor());
7302 fs.insert_tree(
7303 "/root",
7304 json!({
7305 "file1.txt": "content of file1",
7306 "file2.txt": "content of file2",
7307 "dir1": {
7308 "file3.txt": "content of file3"
7309 }
7310 }),
7311 )
7312 .await;
7313
7314 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7315 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7316 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7317 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7318 cx.run_until_parked();
7319
7320 let file1_path = "root/file1.txt";
7321 let file2_path = "root/file2.txt";
7322 select_path_with_mark(&panel, file1_path, cx);
7323 select_path_with_mark(&panel, file2_path, cx);
7324
7325 panel.update_in(cx, |panel, window, cx| {
7326 panel.compare_marked_files(&CompareMarkedFiles, window, cx);
7327 });
7328 cx.executor().run_until_parked();
7329
7330 workspace
7331 .update(cx, |workspace, _, cx| {
7332 let active_items = workspace
7333 .panes()
7334 .iter()
7335 .filter_map(|pane| pane.read(cx).active_item())
7336 .collect::<Vec<_>>();
7337 assert_eq!(active_items.len(), 1);
7338 let diff_view = active_items
7339 .into_iter()
7340 .next()
7341 .unwrap()
7342 .downcast::<FileDiffView>()
7343 .expect("Open item should be an FileDiffView");
7344 assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt");
7345 assert_eq!(
7346 diff_view.tab_tooltip_text(cx).unwrap(),
7347 format!(
7348 "{} ↔ {}",
7349 rel_path(file1_path).display(PathStyle::local()),
7350 rel_path(file2_path).display(PathStyle::local())
7351 )
7352 );
7353 })
7354 .unwrap();
7355
7356 let file1_entry_id = find_project_entry(&panel, file1_path, cx).unwrap();
7357 let file2_entry_id = find_project_entry(&panel, file2_path, cx).unwrap();
7358 let worktree_id = panel.update(cx, |panel, cx| {
7359 panel
7360 .project
7361 .read(cx)
7362 .worktrees(cx)
7363 .next()
7364 .unwrap()
7365 .read(cx)
7366 .id()
7367 });
7368
7369 let expected_entries = [
7370 SelectedEntry {
7371 worktree_id,
7372 entry_id: file1_entry_id,
7373 },
7374 SelectedEntry {
7375 worktree_id,
7376 entry_id: file2_entry_id,
7377 },
7378 ];
7379 panel.update(cx, |panel, _cx| {
7380 assert_eq!(
7381 &panel.marked_entries, &expected_entries,
7382 "Should keep marked entries after comparison"
7383 );
7384 });
7385
7386 panel.update(cx, |panel, cx| {
7387 panel.project.update(cx, |_, cx| {
7388 cx.emit(project::Event::RevealInProjectPanel(file2_entry_id))
7389 })
7390 });
7391
7392 panel.update(cx, |panel, _cx| {
7393 assert_eq!(
7394 &panel.marked_entries, &expected_entries,
7395 "Marked entries should persist after focusing back on the project panel"
7396 );
7397 });
7398}
7399
7400#[gpui::test]
7401async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) {
7402 init_test_with_editor(cx);
7403
7404 let fs = FakeFs::new(cx.executor());
7405 fs.insert_tree(
7406 "/root",
7407 json!({
7408 "file1.txt": "content of file1",
7409 "file2.txt": "content of file2",
7410 "dir1": {},
7411 "dir2": {
7412 "file3.txt": "content of file3"
7413 }
7414 }),
7415 )
7416 .await;
7417
7418 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7419 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7420 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7421 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7422 cx.run_until_parked();
7423
7424 // Test 1: When only one file is selected, there should be no compare option
7425 select_path(&panel, "root/file1.txt", cx);
7426
7427 let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7428 assert_eq!(
7429 selected_files, None,
7430 "Should not have compare option when only one file is selected"
7431 );
7432
7433 // Test 2: When multiple files are selected, there should be a compare option
7434 select_path_with_mark(&panel, "root/file1.txt", cx);
7435 select_path_with_mark(&panel, "root/file2.txt", cx);
7436
7437 let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7438 assert!(
7439 selected_files.is_some(),
7440 "Should have files selected for comparison"
7441 );
7442 if let Some((file1, file2)) = selected_files {
7443 assert!(
7444 file1.to_string_lossy().ends_with("file1.txt")
7445 && file2.to_string_lossy().ends_with("file2.txt"),
7446 "Should have file1.txt and file2.txt as the selected files when multi-selecting"
7447 );
7448 }
7449
7450 // Test 3: Selecting a directory shouldn't count as a comparable file
7451 select_path_with_mark(&panel, "root/dir1", cx);
7452
7453 let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7454 assert!(
7455 selected_files.is_some(),
7456 "Directory selection should not affect comparable files"
7457 );
7458 if let Some((file1, file2)) = selected_files {
7459 assert!(
7460 file1.to_string_lossy().ends_with("file1.txt")
7461 && file2.to_string_lossy().ends_with("file2.txt"),
7462 "Selecting a directory should not affect the number of comparable files"
7463 );
7464 }
7465
7466 // Test 4: Selecting one more file
7467 select_path_with_mark(&panel, "root/dir2/file3.txt", cx);
7468
7469 let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7470 assert!(
7471 selected_files.is_some(),
7472 "Directory selection should not affect comparable files"
7473 );
7474 if let Some((file1, file2)) = selected_files {
7475 assert!(
7476 file1.to_string_lossy().ends_with("file2.txt")
7477 && file2.to_string_lossy().ends_with("file3.txt"),
7478 "Selecting a directory should not affect the number of comparable files"
7479 );
7480 }
7481}
7482
7483#[gpui::test]
7484async fn test_hide_hidden_entries(cx: &mut gpui::TestAppContext) {
7485 init_test(cx);
7486
7487 let fs = FakeFs::new(cx.executor());
7488 fs.insert_tree(
7489 "/root",
7490 json!({
7491 ".hidden-file.txt": "hidden file content",
7492 "visible-file.txt": "visible file content",
7493 ".hidden-parent-dir": {
7494 "nested-dir": {
7495 "file.txt": "file content",
7496 }
7497 },
7498 "visible-dir": {
7499 "file-in-visible.txt": "file content",
7500 "nested": {
7501 ".hidden-nested-dir": {
7502 ".double-hidden-dir": {
7503 "deep-file-1.txt": "deep content 1",
7504 "deep-file-2.txt": "deep content 2"
7505 },
7506 "hidden-nested-file-1.txt": "hidden nested 1",
7507 "hidden-nested-file-2.txt": "hidden nested 2"
7508 },
7509 "visible-nested-file.txt": "visible nested content"
7510 }
7511 }
7512 }),
7513 )
7514 .await;
7515
7516 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7517 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7518 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7519
7520 cx.update(|_, cx| {
7521 let settings = *ProjectPanelSettings::get_global(cx);
7522 ProjectPanelSettings::override_global(
7523 ProjectPanelSettings {
7524 hide_hidden: false,
7525 ..settings
7526 },
7527 cx,
7528 );
7529 });
7530
7531 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7532 cx.run_until_parked();
7533
7534 toggle_expand_dir(&panel, "root/.hidden-parent-dir", cx);
7535 toggle_expand_dir(&panel, "root/.hidden-parent-dir/nested-dir", cx);
7536 toggle_expand_dir(&panel, "root/visible-dir", cx);
7537 toggle_expand_dir(&panel, "root/visible-dir/nested", cx);
7538 toggle_expand_dir(&panel, "root/visible-dir/nested/.hidden-nested-dir", cx);
7539 toggle_expand_dir(
7540 &panel,
7541 "root/visible-dir/nested/.hidden-nested-dir/.double-hidden-dir",
7542 cx,
7543 );
7544
7545 let expanded = [
7546 "v root",
7547 " v .hidden-parent-dir",
7548 " v nested-dir",
7549 " file.txt",
7550 " v visible-dir",
7551 " v nested",
7552 " v .hidden-nested-dir",
7553 " v .double-hidden-dir <== selected",
7554 " deep-file-1.txt",
7555 " deep-file-2.txt",
7556 " hidden-nested-file-1.txt",
7557 " hidden-nested-file-2.txt",
7558 " visible-nested-file.txt",
7559 " file-in-visible.txt",
7560 " .hidden-file.txt",
7561 " visible-file.txt",
7562 ];
7563
7564 assert_eq!(
7565 visible_entries_as_strings(&panel, 0..30, cx),
7566 &expanded,
7567 "With hide_hidden=false, contents of hidden nested directory should be visible"
7568 );
7569
7570 cx.update(|_, cx| {
7571 let settings = *ProjectPanelSettings::get_global(cx);
7572 ProjectPanelSettings::override_global(
7573 ProjectPanelSettings {
7574 hide_hidden: true,
7575 ..settings
7576 },
7577 cx,
7578 );
7579 });
7580
7581 panel.update_in(cx, |panel, window, cx| {
7582 panel.update_visible_entries(None, false, false, window, cx);
7583 });
7584 cx.run_until_parked();
7585
7586 assert_eq!(
7587 visible_entries_as_strings(&panel, 0..30, cx),
7588 &[
7589 "v root",
7590 " v visible-dir",
7591 " v nested",
7592 " visible-nested-file.txt",
7593 " file-in-visible.txt",
7594 " visible-file.txt",
7595 ],
7596 "With hide_hidden=false, contents of hidden nested directory should be visible"
7597 );
7598
7599 panel.update_in(cx, |panel, window, cx| {
7600 let settings = *ProjectPanelSettings::get_global(cx);
7601 ProjectPanelSettings::override_global(
7602 ProjectPanelSettings {
7603 hide_hidden: false,
7604 ..settings
7605 },
7606 cx,
7607 );
7608 panel.update_visible_entries(None, false, false, window, cx);
7609 });
7610 cx.run_until_parked();
7611
7612 assert_eq!(
7613 visible_entries_as_strings(&panel, 0..30, cx),
7614 &expanded,
7615 "With hide_hidden=false, deeply nested hidden directories and their contents should be visible"
7616 );
7617}
7618
7619fn select_path(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
7620 let path = rel_path(path);
7621 panel.update_in(cx, |panel, window, cx| {
7622 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
7623 let worktree = worktree.read(cx);
7624 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
7625 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
7626 panel.update_visible_entries(
7627 Some((worktree.id(), entry_id)),
7628 false,
7629 false,
7630 window,
7631 cx,
7632 );
7633 return;
7634 }
7635 }
7636 panic!("no worktree for path {:?}", path);
7637 });
7638 cx.run_until_parked();
7639}
7640
7641fn select_path_with_mark(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
7642 let path = rel_path(path);
7643 panel.update(cx, |panel, cx| {
7644 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
7645 let worktree = worktree.read(cx);
7646 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
7647 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
7648 let entry = crate::SelectedEntry {
7649 worktree_id: worktree.id(),
7650 entry_id,
7651 };
7652 if !panel.marked_entries.contains(&entry) {
7653 panel.marked_entries.push(entry);
7654 }
7655 panel.state.selection = Some(entry);
7656 return;
7657 }
7658 }
7659 panic!("no worktree for path {:?}", path);
7660 });
7661}
7662
7663fn drag_selection_to(
7664 panel: &Entity<ProjectPanel>,
7665 target_path: &str,
7666 is_file: bool,
7667 cx: &mut VisualTestContext,
7668) {
7669 let target_entry = find_project_entry(panel, target_path, cx)
7670 .unwrap_or_else(|| panic!("no entry for target path {target_path:?}"));
7671
7672 panel.update_in(cx, |panel, window, cx| {
7673 let selection = panel
7674 .state
7675 .selection
7676 .expect("a selection is required before dragging");
7677 let drag = DraggedSelection {
7678 active_selection: SelectedEntry {
7679 worktree_id: selection.worktree_id,
7680 entry_id: panel.resolve_entry(selection.entry_id),
7681 },
7682 marked_selections: Arc::from(panel.marked_entries.clone()),
7683 };
7684 panel.drag_onto(&drag, target_entry, is_file, window, cx);
7685 });
7686 cx.executor().run_until_parked();
7687}
7688
7689fn find_project_entry(
7690 panel: &Entity<ProjectPanel>,
7691 path: &str,
7692 cx: &mut VisualTestContext,
7693) -> Option<ProjectEntryId> {
7694 let path = rel_path(path);
7695 panel.update(cx, |panel, cx| {
7696 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
7697 let worktree = worktree.read(cx);
7698 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
7699 return worktree.entry_for_path(relative_path).map(|entry| entry.id);
7700 }
7701 }
7702 panic!("no worktree for path {path:?}");
7703 })
7704}
7705
7706fn visible_entries_as_strings(
7707 panel: &Entity<ProjectPanel>,
7708 range: Range<usize>,
7709 cx: &mut VisualTestContext,
7710) -> Vec<String> {
7711 let mut result = Vec::new();
7712 let mut project_entries = HashSet::default();
7713 let mut has_editor = false;
7714
7715 panel.update_in(cx, |panel, window, cx| {
7716 panel.for_each_visible_entry(range, window, cx, |project_entry, details, _, _| {
7717 if details.is_editing {
7718 assert!(!has_editor, "duplicate editor entry");
7719 has_editor = true;
7720 } else {
7721 assert!(
7722 project_entries.insert(project_entry),
7723 "duplicate project entry {:?} {:?}",
7724 project_entry,
7725 details
7726 );
7727 }
7728
7729 let indent = " ".repeat(details.depth);
7730 let icon = if details.kind.is_dir() {
7731 if details.is_expanded { "v " } else { "> " }
7732 } else {
7733 " "
7734 };
7735 #[cfg(windows)]
7736 let filename = details.filename.replace("\\", "/");
7737 #[cfg(not(windows))]
7738 let filename = details.filename;
7739 let name = if details.is_editing {
7740 format!("[EDITOR: '{}']", filename)
7741 } else if details.is_processing {
7742 format!("[PROCESSING: '{}']", filename)
7743 } else {
7744 filename
7745 };
7746 let selected = if details.is_selected {
7747 " <== selected"
7748 } else {
7749 ""
7750 };
7751 let marked = if details.is_marked {
7752 " <== marked"
7753 } else {
7754 ""
7755 };
7756
7757 result.push(format!("{indent}{icon}{name}{selected}{marked}"));
7758 });
7759 });
7760
7761 result
7762}
7763
7764/// Test that missing sort_mode field defaults to DirectoriesFirst
7765#[gpui::test]
7766async fn test_sort_mode_default_fallback(cx: &mut gpui::TestAppContext) {
7767 init_test(cx);
7768
7769 // Verify that when sort_mode is not specified, it defaults to DirectoriesFirst
7770 let default_settings = cx.read(|cx| *ProjectPanelSettings::get_global(cx));
7771 assert_eq!(
7772 default_settings.sort_mode,
7773 settings::ProjectPanelSortMode::DirectoriesFirst,
7774 "sort_mode should default to DirectoriesFirst"
7775 );
7776}
7777
7778/// Test sort modes: DirectoriesFirst (default) vs Mixed
7779#[gpui::test]
7780async fn test_sort_mode_directories_first(cx: &mut gpui::TestAppContext) {
7781 init_test(cx);
7782
7783 let fs = FakeFs::new(cx.executor());
7784 fs.insert_tree(
7785 "/root",
7786 json!({
7787 "zebra.txt": "",
7788 "Apple": {},
7789 "banana.rs": "",
7790 "Carrot": {},
7791 "aardvark.txt": "",
7792 }),
7793 )
7794 .await;
7795
7796 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7797 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7798 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7799 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7800 cx.run_until_parked();
7801
7802 // Default sort mode should be DirectoriesFirst
7803 assert_eq!(
7804 visible_entries_as_strings(&panel, 0..50, cx),
7805 &[
7806 "v root",
7807 " > Apple",
7808 " > Carrot",
7809 " aardvark.txt",
7810 " banana.rs",
7811 " zebra.txt",
7812 ]
7813 );
7814}
7815
7816#[gpui::test]
7817async fn test_sort_mode_mixed(cx: &mut gpui::TestAppContext) {
7818 init_test(cx);
7819
7820 let fs = FakeFs::new(cx.executor());
7821 fs.insert_tree(
7822 "/root",
7823 json!({
7824 "Zebra.txt": "",
7825 "apple": {},
7826 "Banana.rs": "",
7827 "carrot": {},
7828 "Aardvark.txt": "",
7829 }),
7830 )
7831 .await;
7832
7833 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7834 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7835 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7836
7837 // Switch to Mixed mode
7838 cx.update(|_, cx| {
7839 cx.update_global::<SettingsStore, _>(|store, cx| {
7840 store.update_user_settings(cx, |settings| {
7841 settings.project_panel.get_or_insert_default().sort_mode =
7842 Some(settings::ProjectPanelSortMode::Mixed);
7843 });
7844 });
7845 });
7846
7847 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7848 cx.run_until_parked();
7849
7850 // Mixed mode: case-insensitive sorting
7851 // Aardvark < apple < Banana < carrot < Zebra (all case-insensitive)
7852 assert_eq!(
7853 visible_entries_as_strings(&panel, 0..50, cx),
7854 &[
7855 "v root",
7856 " Aardvark.txt",
7857 " > apple",
7858 " Banana.rs",
7859 " > carrot",
7860 " Zebra.txt",
7861 ]
7862 );
7863}
7864
7865#[gpui::test]
7866async fn test_sort_mode_files_first(cx: &mut gpui::TestAppContext) {
7867 init_test(cx);
7868
7869 let fs = FakeFs::new(cx.executor());
7870 fs.insert_tree(
7871 "/root",
7872 json!({
7873 "Zebra.txt": "",
7874 "apple": {},
7875 "Banana.rs": "",
7876 "carrot": {},
7877 "Aardvark.txt": "",
7878 }),
7879 )
7880 .await;
7881
7882 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7883 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7884 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7885
7886 // Switch to FilesFirst mode
7887 cx.update(|_, cx| {
7888 cx.update_global::<SettingsStore, _>(|store, cx| {
7889 store.update_user_settings(cx, |settings| {
7890 settings.project_panel.get_or_insert_default().sort_mode =
7891 Some(settings::ProjectPanelSortMode::FilesFirst);
7892 });
7893 });
7894 });
7895
7896 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7897 cx.run_until_parked();
7898
7899 // FilesFirst mode: files first, then directories (both case-insensitive)
7900 assert_eq!(
7901 visible_entries_as_strings(&panel, 0..50, cx),
7902 &[
7903 "v root",
7904 " Aardvark.txt",
7905 " Banana.rs",
7906 " Zebra.txt",
7907 " > apple",
7908 " > carrot",
7909 ]
7910 );
7911}
7912
7913#[gpui::test]
7914async fn test_sort_mode_toggle(cx: &mut gpui::TestAppContext) {
7915 init_test(cx);
7916
7917 let fs = FakeFs::new(cx.executor());
7918 fs.insert_tree(
7919 "/root",
7920 json!({
7921 "file2.txt": "",
7922 "dir1": {},
7923 "file1.txt": "",
7924 }),
7925 )
7926 .await;
7927
7928 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7929 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7930 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7931 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7932 cx.run_until_parked();
7933
7934 // Initially DirectoriesFirst
7935 assert_eq!(
7936 visible_entries_as_strings(&panel, 0..50, cx),
7937 &["v root", " > dir1", " file1.txt", " file2.txt",]
7938 );
7939
7940 // Toggle to Mixed
7941 cx.update(|_, cx| {
7942 cx.update_global::<SettingsStore, _>(|store, cx| {
7943 store.update_user_settings(cx, |settings| {
7944 settings.project_panel.get_or_insert_default().sort_mode =
7945 Some(settings::ProjectPanelSortMode::Mixed);
7946 });
7947 });
7948 });
7949 cx.run_until_parked();
7950
7951 assert_eq!(
7952 visible_entries_as_strings(&panel, 0..50, cx),
7953 &["v root", " > dir1", " file1.txt", " file2.txt",]
7954 );
7955
7956 // Toggle back to DirectoriesFirst
7957 cx.update(|_, cx| {
7958 cx.update_global::<SettingsStore, _>(|store, cx| {
7959 store.update_user_settings(cx, |settings| {
7960 settings.project_panel.get_or_insert_default().sort_mode =
7961 Some(settings::ProjectPanelSortMode::DirectoriesFirst);
7962 });
7963 });
7964 });
7965 cx.run_until_parked();
7966
7967 assert_eq!(
7968 visible_entries_as_strings(&panel, 0..50, cx),
7969 &["v root", " > dir1", " file1.txt", " file2.txt",]
7970 );
7971}
7972
7973fn init_test(cx: &mut TestAppContext) {
7974 cx.update(|cx| {
7975 let settings_store = SettingsStore::test(cx);
7976 cx.set_global(settings_store);
7977 theme::init(theme::LoadThemes::JustBase, cx);
7978 crate::init(cx);
7979
7980 cx.update_global::<SettingsStore, _>(|store, cx| {
7981 store.update_user_settings(cx, |settings| {
7982 settings
7983 .project_panel
7984 .get_or_insert_default()
7985 .auto_fold_dirs = Some(false);
7986 settings.project.worktree.file_scan_exclusions = Some(Vec::new());
7987 });
7988 });
7989 });
7990}
7991
7992fn init_test_with_editor(cx: &mut TestAppContext) {
7993 cx.update(|cx| {
7994 let app_state = AppState::test(cx);
7995 theme::init(theme::LoadThemes::JustBase, cx);
7996 editor::init(cx);
7997 crate::init(cx);
7998 workspace::init(app_state, cx);
7999
8000 cx.update_global::<SettingsStore, _>(|store, cx| {
8001 store.update_user_settings(cx, |settings| {
8002 settings
8003 .project_panel
8004 .get_or_insert_default()
8005 .auto_fold_dirs = Some(false);
8006 settings.project.worktree.file_scan_exclusions = Some(Vec::new())
8007 });
8008 });
8009 });
8010}
8011
8012fn set_auto_open_settings(
8013 cx: &mut TestAppContext,
8014 auto_open_settings: ProjectPanelAutoOpenSettings,
8015) {
8016 cx.update(|cx| {
8017 cx.update_global::<SettingsStore, _>(|store, cx| {
8018 store.update_user_settings(cx, |settings| {
8019 settings.project_panel.get_or_insert_default().auto_open = Some(auto_open_settings);
8020 });
8021 })
8022 });
8023}
8024
8025fn ensure_single_file_is_opened(
8026 window: &WindowHandle<Workspace>,
8027 expected_path: &str,
8028 cx: &mut TestAppContext,
8029) {
8030 window
8031 .update(cx, |workspace, _, cx| {
8032 let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
8033 assert_eq!(worktrees.len(), 1);
8034 let worktree_id = worktrees[0].read(cx).id();
8035
8036 let open_project_paths = workspace
8037 .panes()
8038 .iter()
8039 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
8040 .collect::<Vec<_>>();
8041 assert_eq!(
8042 open_project_paths,
8043 vec![ProjectPath {
8044 worktree_id,
8045 path: Arc::from(rel_path(expected_path))
8046 }],
8047 "Should have opened file, selected in project panel"
8048 );
8049 })
8050 .unwrap();
8051}
8052
8053fn submit_deletion(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
8054 assert!(
8055 !cx.has_pending_prompt(),
8056 "Should have no prompts before the deletion"
8057 );
8058 panel.update_in(cx, |panel, window, cx| {
8059 panel.delete(&Delete { skip_prompt: false }, window, cx)
8060 });
8061 assert!(
8062 cx.has_pending_prompt(),
8063 "Should have a prompt after the deletion"
8064 );
8065 cx.simulate_prompt_answer("Delete");
8066 assert!(
8067 !cx.has_pending_prompt(),
8068 "Should have no prompts after prompt was replied to"
8069 );
8070 cx.executor().run_until_parked();
8071}
8072
8073fn submit_deletion_skipping_prompt(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
8074 assert!(
8075 !cx.has_pending_prompt(),
8076 "Should have no prompts before the deletion"
8077 );
8078 panel.update_in(cx, |panel, window, cx| {
8079 panel.delete(&Delete { skip_prompt: true }, window, cx)
8080 });
8081 assert!(!cx.has_pending_prompt(), "Should have received no prompts");
8082 cx.executor().run_until_parked();
8083}
8084
8085fn ensure_no_open_items_and_panes(workspace: &WindowHandle<Workspace>, cx: &mut VisualTestContext) {
8086 assert!(
8087 !cx.has_pending_prompt(),
8088 "Should have no prompts after deletion operation closes the file"
8089 );
8090 workspace
8091 .read_with(cx, |workspace, cx| {
8092 let open_project_paths = workspace
8093 .panes()
8094 .iter()
8095 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
8096 .collect::<Vec<_>>();
8097 assert!(
8098 open_project_paths.is_empty(),
8099 "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
8100 );
8101 })
8102 .unwrap();
8103}
8104
8105struct TestProjectItemView {
8106 focus_handle: FocusHandle,
8107 path: ProjectPath,
8108}
8109
8110struct TestProjectItem {
8111 path: ProjectPath,
8112}
8113
8114impl project::ProjectItem for TestProjectItem {
8115 fn try_open(
8116 _project: &Entity<Project>,
8117 path: &ProjectPath,
8118 cx: &mut App,
8119 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
8120 let path = path.clone();
8121 Some(cx.spawn(async move |cx| cx.new(|_| Self { path })))
8122 }
8123
8124 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8125 None
8126 }
8127
8128 fn project_path(&self, _: &App) -> Option<ProjectPath> {
8129 Some(self.path.clone())
8130 }
8131
8132 fn is_dirty(&self) -> bool {
8133 false
8134 }
8135}
8136
8137impl ProjectItem for TestProjectItemView {
8138 type Item = TestProjectItem;
8139
8140 fn for_project_item(
8141 _: Entity<Project>,
8142 _: Option<&Pane>,
8143 project_item: Entity<Self::Item>,
8144 _: &mut Window,
8145 cx: &mut Context<Self>,
8146 ) -> Self
8147 where
8148 Self: Sized,
8149 {
8150 Self {
8151 path: project_item.update(cx, |project_item, _| project_item.path.clone()),
8152 focus_handle: cx.focus_handle(),
8153 }
8154 }
8155}
8156
8157impl Item for TestProjectItemView {
8158 type Event = ();
8159
8160 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
8161 "Test".into()
8162 }
8163}
8164
8165impl EventEmitter<()> for TestProjectItemView {}
8166
8167impl Focusable for TestProjectItemView {
8168 fn focus_handle(&self, _: &App) -> FocusHandle {
8169 self.focus_handle.clone()
8170 }
8171}
8172
8173impl Render for TestProjectItemView {
8174 fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
8175 Empty
8176 }
8177}