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_remove_opened_file(cx: &mut gpui::TestAppContext) {
1922 init_test_with_editor(cx);
1923
1924 let fs = FakeFs::new(cx.executor());
1925 fs.insert_tree(
1926 path!("/src"),
1927 json!({
1928 "test": {
1929 "first.rs": "// First Rust file",
1930 "second.rs": "// Second Rust file",
1931 "third.rs": "// Third Rust file",
1932 }
1933 }),
1934 )
1935 .await;
1936
1937 let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await;
1938 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1939 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1940 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1941 cx.run_until_parked();
1942
1943 toggle_expand_dir(&panel, "src/test", cx);
1944 select_path(&panel, "src/test/first.rs", cx);
1945 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
1946 cx.executor().run_until_parked();
1947 assert_eq!(
1948 visible_entries_as_strings(&panel, 0..10, cx),
1949 &[
1950 "v src",
1951 " v test",
1952 " first.rs <== selected <== marked",
1953 " second.rs",
1954 " third.rs"
1955 ]
1956 );
1957 ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
1958
1959 submit_deletion(&panel, cx);
1960 assert_eq!(
1961 visible_entries_as_strings(&panel, 0..10, cx),
1962 &[
1963 "v src",
1964 " v test",
1965 " second.rs <== selected",
1966 " third.rs"
1967 ],
1968 "Project panel should have no deleted file, no other file is selected in it"
1969 );
1970 ensure_no_open_items_and_panes(&workspace, cx);
1971
1972 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
1973 cx.executor().run_until_parked();
1974 assert_eq!(
1975 visible_entries_as_strings(&panel, 0..10, cx),
1976 &[
1977 "v src",
1978 " v test",
1979 " second.rs <== selected <== marked",
1980 " third.rs"
1981 ]
1982 );
1983 ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
1984
1985 workspace
1986 .update(cx, |workspace, window, cx| {
1987 let active_items = workspace
1988 .panes()
1989 .iter()
1990 .filter_map(|pane| pane.read(cx).active_item())
1991 .collect::<Vec<_>>();
1992 assert_eq!(active_items.len(), 1);
1993 let open_editor = active_items
1994 .into_iter()
1995 .next()
1996 .unwrap()
1997 .downcast::<Editor>()
1998 .expect("Open item should be an editor");
1999 open_editor.update(cx, |editor, cx| {
2000 editor.set_text("Another text!", window, cx)
2001 });
2002 })
2003 .unwrap();
2004 submit_deletion_skipping_prompt(&panel, cx);
2005 assert_eq!(
2006 visible_entries_as_strings(&panel, 0..10, cx),
2007 &["v src", " v test", " third.rs <== selected"],
2008 "Project panel should have no deleted file, with one last file remaining"
2009 );
2010 ensure_no_open_items_and_panes(&workspace, cx);
2011}
2012
2013#[gpui::test]
2014async fn test_auto_open_new_file_when_enabled(cx: &mut gpui::TestAppContext) {
2015 init_test_with_editor(cx);
2016 set_auto_open_settings(
2017 cx,
2018 ProjectPanelAutoOpenSettings {
2019 on_create: Some(true),
2020 ..Default::default()
2021 },
2022 );
2023
2024 let fs = FakeFs::new(cx.executor());
2025 fs.insert_tree(path!("/root"), json!({})).await;
2026
2027 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2028 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2029 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2030 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2031 cx.run_until_parked();
2032
2033 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2034 cx.run_until_parked();
2035 panel
2036 .update_in(cx, |panel, window, cx| {
2037 panel.filename_editor.update(cx, |editor, cx| {
2038 editor.set_text("auto-open.rs", window, cx);
2039 });
2040 panel.confirm_edit(true, window, cx).unwrap()
2041 })
2042 .await
2043 .unwrap();
2044 cx.run_until_parked();
2045
2046 ensure_single_file_is_opened(&workspace, "auto-open.rs", cx);
2047}
2048
2049#[gpui::test]
2050async fn test_auto_open_new_file_when_disabled(cx: &mut gpui::TestAppContext) {
2051 init_test_with_editor(cx);
2052 set_auto_open_settings(
2053 cx,
2054 ProjectPanelAutoOpenSettings {
2055 on_create: Some(false),
2056 ..Default::default()
2057 },
2058 );
2059
2060 let fs = FakeFs::new(cx.executor());
2061 fs.insert_tree(path!("/root"), json!({})).await;
2062
2063 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2064 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2065 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2066 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2067 cx.run_until_parked();
2068
2069 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2070 cx.run_until_parked();
2071 panel
2072 .update_in(cx, |panel, window, cx| {
2073 panel.filename_editor.update(cx, |editor, cx| {
2074 editor.set_text("manual-open.rs", window, cx);
2075 });
2076 panel.confirm_edit(true, window, cx).unwrap()
2077 })
2078 .await
2079 .unwrap();
2080 cx.run_until_parked();
2081
2082 ensure_no_open_items_and_panes(&workspace, cx);
2083}
2084
2085#[gpui::test]
2086async fn test_auto_open_on_paste_when_enabled(cx: &mut gpui::TestAppContext) {
2087 init_test_with_editor(cx);
2088 set_auto_open_settings(
2089 cx,
2090 ProjectPanelAutoOpenSettings {
2091 on_paste: Some(true),
2092 ..Default::default()
2093 },
2094 );
2095
2096 let fs = FakeFs::new(cx.executor());
2097 fs.insert_tree(
2098 path!("/root"),
2099 json!({
2100 "src": {
2101 "original.rs": ""
2102 },
2103 "target": {}
2104 }),
2105 )
2106 .await;
2107
2108 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2109 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2110 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2111 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2112 cx.run_until_parked();
2113
2114 toggle_expand_dir(&panel, "root/src", cx);
2115 toggle_expand_dir(&panel, "root/target", cx);
2116
2117 select_path(&panel, "root/src/original.rs", cx);
2118 panel.update_in(cx, |panel, window, cx| {
2119 panel.copy(&Default::default(), window, cx);
2120 });
2121
2122 select_path(&panel, "root/target", cx);
2123 panel.update_in(cx, |panel, window, cx| {
2124 panel.paste(&Default::default(), window, cx);
2125 });
2126 cx.executor().run_until_parked();
2127
2128 ensure_single_file_is_opened(&workspace, "target/original.rs", cx);
2129}
2130
2131#[gpui::test]
2132async fn test_auto_open_on_paste_when_disabled(cx: &mut gpui::TestAppContext) {
2133 init_test_with_editor(cx);
2134 set_auto_open_settings(
2135 cx,
2136 ProjectPanelAutoOpenSettings {
2137 on_paste: Some(false),
2138 ..Default::default()
2139 },
2140 );
2141
2142 let fs = FakeFs::new(cx.executor());
2143 fs.insert_tree(
2144 path!("/root"),
2145 json!({
2146 "src": {
2147 "original.rs": ""
2148 },
2149 "target": {}
2150 }),
2151 )
2152 .await;
2153
2154 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2155 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2156 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2157 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2158 cx.run_until_parked();
2159
2160 toggle_expand_dir(&panel, "root/src", cx);
2161 toggle_expand_dir(&panel, "root/target", cx);
2162
2163 select_path(&panel, "root/src/original.rs", cx);
2164 panel.update_in(cx, |panel, window, cx| {
2165 panel.copy(&Default::default(), window, cx);
2166 });
2167
2168 select_path(&panel, "root/target", cx);
2169 panel.update_in(cx, |panel, window, cx| {
2170 panel.paste(&Default::default(), window, cx);
2171 });
2172 cx.executor().run_until_parked();
2173
2174 ensure_no_open_items_and_panes(&workspace, cx);
2175 assert!(
2176 find_project_entry(&panel, "root/target/original.rs", cx).is_some(),
2177 "Pasted entry should exist even when auto-open is disabled"
2178 );
2179}
2180
2181#[gpui::test]
2182async fn test_auto_open_on_drop_when_enabled(cx: &mut gpui::TestAppContext) {
2183 init_test_with_editor(cx);
2184 set_auto_open_settings(
2185 cx,
2186 ProjectPanelAutoOpenSettings {
2187 on_drop: Some(true),
2188 ..Default::default()
2189 },
2190 );
2191
2192 let fs = FakeFs::new(cx.executor());
2193 fs.insert_tree(path!("/root"), json!({})).await;
2194
2195 let temp_dir = tempfile::tempdir().unwrap();
2196 let external_path = temp_dir.path().join("dropped.rs");
2197 std::fs::write(&external_path, "// dropped").unwrap();
2198 fs.insert_tree_from_real_fs(temp_dir.path(), temp_dir.path())
2199 .await;
2200
2201 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2202 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2203 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2204 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2205 cx.run_until_parked();
2206
2207 let root_entry = find_project_entry(&panel, "root", cx).unwrap();
2208 panel.update_in(cx, |panel, window, cx| {
2209 panel.drop_external_files(std::slice::from_ref(&external_path), root_entry, window, cx);
2210 });
2211 cx.executor().run_until_parked();
2212
2213 ensure_single_file_is_opened(&workspace, "dropped.rs", cx);
2214}
2215
2216#[gpui::test]
2217async fn test_auto_open_on_drop_when_disabled(cx: &mut gpui::TestAppContext) {
2218 init_test_with_editor(cx);
2219 set_auto_open_settings(
2220 cx,
2221 ProjectPanelAutoOpenSettings {
2222 on_drop: Some(false),
2223 ..Default::default()
2224 },
2225 );
2226
2227 let fs = FakeFs::new(cx.executor());
2228 fs.insert_tree(path!("/root"), json!({})).await;
2229
2230 let temp_dir = tempfile::tempdir().unwrap();
2231 let external_path = temp_dir.path().join("manual.rs");
2232 std::fs::write(&external_path, "// dropped").unwrap();
2233 fs.insert_tree_from_real_fs(temp_dir.path(), temp_dir.path())
2234 .await;
2235
2236 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2237 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2238 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2239 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2240 cx.run_until_parked();
2241
2242 let root_entry = find_project_entry(&panel, "root", cx).unwrap();
2243 panel.update_in(cx, |panel, window, cx| {
2244 panel.drop_external_files(std::slice::from_ref(&external_path), root_entry, window, cx);
2245 });
2246 cx.executor().run_until_parked();
2247
2248 ensure_no_open_items_and_panes(&workspace, cx);
2249 assert!(
2250 find_project_entry(&panel, "root/manual.rs", cx).is_some(),
2251 "Dropped entry should exist even when auto-open is disabled"
2252 );
2253}
2254
2255#[gpui::test]
2256async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
2257 init_test_with_editor(cx);
2258
2259 let fs = FakeFs::new(cx.executor());
2260 fs.insert_tree(
2261 "/src",
2262 json!({
2263 "test": {
2264 "first.rs": "// First Rust file",
2265 "second.rs": "// Second Rust file",
2266 "third.rs": "// Third Rust file",
2267 }
2268 }),
2269 )
2270 .await;
2271
2272 let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2273 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2274 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2275 let panel = workspace
2276 .update(cx, |workspace, window, cx| {
2277 let panel = ProjectPanel::new(workspace, window, cx);
2278 workspace.add_panel(panel.clone(), window, cx);
2279 panel
2280 })
2281 .unwrap();
2282 cx.run_until_parked();
2283
2284 select_path(&panel, "src", cx);
2285 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2286 cx.executor().run_until_parked();
2287 assert_eq!(
2288 visible_entries_as_strings(&panel, 0..10, cx),
2289 &[
2290 //
2291 "v src <== selected",
2292 " > test"
2293 ]
2294 );
2295 panel.update_in(cx, |panel, window, cx| {
2296 panel.new_directory(&NewDirectory, window, cx)
2297 });
2298 cx.run_until_parked();
2299 panel.update_in(cx, |panel, window, cx| {
2300 assert!(panel.filename_editor.read(cx).is_focused(window));
2301 });
2302 cx.executor().run_until_parked();
2303 assert_eq!(
2304 visible_entries_as_strings(&panel, 0..10, cx),
2305 &[
2306 //
2307 "v src",
2308 " > [EDITOR: ''] <== selected",
2309 " > test"
2310 ]
2311 );
2312 panel.update_in(cx, |panel, window, cx| {
2313 panel
2314 .filename_editor
2315 .update(cx, |editor, cx| editor.set_text("test", window, cx));
2316 assert!(
2317 panel.confirm_edit(true, window, cx).is_none(),
2318 "Should not allow to confirm on conflicting new directory name"
2319 );
2320 });
2321 cx.executor().run_until_parked();
2322 panel.update_in(cx, |panel, window, cx| {
2323 assert!(
2324 panel.state.edit_state.is_some(),
2325 "Edit state should not be None after conflicting new directory name"
2326 );
2327 panel.cancel(&menu::Cancel, window, cx);
2328 panel.update_visible_entries(None, false, false, window, cx);
2329 });
2330 cx.run_until_parked();
2331 assert_eq!(
2332 visible_entries_as_strings(&panel, 0..10, cx),
2333 &[
2334 //
2335 "v src <== selected",
2336 " > test"
2337 ],
2338 "File list should be unchanged after failed folder create confirmation"
2339 );
2340
2341 select_path(&panel, "src/test", cx);
2342 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2343 cx.executor().run_until_parked();
2344 assert_eq!(
2345 visible_entries_as_strings(&panel, 0..10, cx),
2346 &[
2347 //
2348 "v src",
2349 " > test <== selected"
2350 ]
2351 );
2352 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2353 cx.run_until_parked();
2354 panel.update_in(cx, |panel, window, cx| {
2355 assert!(panel.filename_editor.read(cx).is_focused(window));
2356 });
2357 assert_eq!(
2358 visible_entries_as_strings(&panel, 0..10, cx),
2359 &[
2360 "v src",
2361 " v test",
2362 " [EDITOR: ''] <== selected",
2363 " first.rs",
2364 " second.rs",
2365 " third.rs"
2366 ]
2367 );
2368 panel.update_in(cx, |panel, window, cx| {
2369 panel
2370 .filename_editor
2371 .update(cx, |editor, cx| editor.set_text("first.rs", window, cx));
2372 assert!(
2373 panel.confirm_edit(true, window, cx).is_none(),
2374 "Should not allow to confirm on conflicting new file name"
2375 );
2376 });
2377 cx.executor().run_until_parked();
2378 panel.update_in(cx, |panel, window, cx| {
2379 assert!(
2380 panel.state.edit_state.is_some(),
2381 "Edit state should not be None after conflicting new file name"
2382 );
2383 panel.cancel(&menu::Cancel, window, cx);
2384 panel.update_visible_entries(None, false, false, window, cx);
2385 });
2386 cx.run_until_parked();
2387 assert_eq!(
2388 visible_entries_as_strings(&panel, 0..10, cx),
2389 &[
2390 "v src",
2391 " v test <== selected",
2392 " first.rs",
2393 " second.rs",
2394 " third.rs"
2395 ],
2396 "File list should be unchanged after failed file create confirmation"
2397 );
2398
2399 select_path(&panel, "src/test/first.rs", cx);
2400 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2401 cx.executor().run_until_parked();
2402 assert_eq!(
2403 visible_entries_as_strings(&panel, 0..10, cx),
2404 &[
2405 "v src",
2406 " v test",
2407 " first.rs <== selected",
2408 " second.rs",
2409 " third.rs"
2410 ],
2411 );
2412 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2413 cx.executor().run_until_parked();
2414 panel.update_in(cx, |panel, window, cx| {
2415 assert!(panel.filename_editor.read(cx).is_focused(window));
2416 });
2417 assert_eq!(
2418 visible_entries_as_strings(&panel, 0..10, cx),
2419 &[
2420 "v src",
2421 " v test",
2422 " [EDITOR: 'first.rs'] <== selected",
2423 " second.rs",
2424 " third.rs"
2425 ]
2426 );
2427 panel.update_in(cx, |panel, window, cx| {
2428 panel
2429 .filename_editor
2430 .update(cx, |editor, cx| editor.set_text("second.rs", window, cx));
2431 assert!(
2432 panel.confirm_edit(true, window, cx).is_none(),
2433 "Should not allow to confirm on conflicting file rename"
2434 )
2435 });
2436 cx.executor().run_until_parked();
2437 panel.update_in(cx, |panel, window, cx| {
2438 assert!(
2439 panel.state.edit_state.is_some(),
2440 "Edit state should not be None after conflicting file rename"
2441 );
2442 panel.cancel(&menu::Cancel, window, cx);
2443 });
2444 assert_eq!(
2445 visible_entries_as_strings(&panel, 0..10, cx),
2446 &[
2447 "v src",
2448 " v test",
2449 " first.rs <== selected",
2450 " second.rs",
2451 " third.rs"
2452 ],
2453 "File list should be unchanged after failed rename confirmation"
2454 );
2455}
2456
2457// NOTE: This test is skipped on Windows, because on Windows,
2458// when it triggers the lsp store it converts `/src/test/first copy.txt` into an uri
2459// but it fails with message `"/src\\test\\first copy.txt" is not parseable as an URI`
2460#[gpui::test]
2461#[cfg_attr(target_os = "windows", ignore)]
2462async fn test_create_duplicate_items_and_check_history(cx: &mut gpui::TestAppContext) {
2463 init_test_with_editor(cx);
2464
2465 let fs = FakeFs::new(cx.executor());
2466 fs.insert_tree(
2467 "/src",
2468 json!({
2469 "test": {
2470 "first.txt": "// First Txt file",
2471 "second.txt": "// Second Txt file",
2472 "third.txt": "// Third Txt file",
2473 }
2474 }),
2475 )
2476 .await;
2477
2478 let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2479 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2480 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2481 let panel = workspace
2482 .update(cx, |workspace, window, cx| {
2483 let panel = ProjectPanel::new(workspace, window, cx);
2484 workspace.add_panel(panel.clone(), window, cx);
2485 panel
2486 })
2487 .unwrap();
2488 cx.run_until_parked();
2489
2490 select_path(&panel, "src", cx);
2491 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2492 cx.executor().run_until_parked();
2493 assert_eq!(
2494 visible_entries_as_strings(&panel, 0..10, cx),
2495 &[
2496 //
2497 "v src <== selected",
2498 " > test"
2499 ]
2500 );
2501 panel.update_in(cx, |panel, window, cx| {
2502 panel.new_directory(&NewDirectory, window, cx)
2503 });
2504 cx.run_until_parked();
2505 panel.update_in(cx, |panel, window, cx| {
2506 assert!(panel.filename_editor.read(cx).is_focused(window));
2507 });
2508 cx.executor().run_until_parked();
2509 assert_eq!(
2510 visible_entries_as_strings(&panel, 0..10, cx),
2511 &[
2512 //
2513 "v src",
2514 " > [EDITOR: ''] <== selected",
2515 " > test"
2516 ]
2517 );
2518 panel.update_in(cx, |panel, window, cx| {
2519 panel
2520 .filename_editor
2521 .update(cx, |editor, cx| editor.set_text("test", window, cx));
2522 assert!(
2523 panel.confirm_edit(true, window, cx).is_none(),
2524 "Should not allow to confirm on conflicting new directory name"
2525 );
2526 });
2527 cx.executor().run_until_parked();
2528 panel.update_in(cx, |panel, window, cx| {
2529 assert!(
2530 panel.state.edit_state.is_some(),
2531 "Edit state should not be None after conflicting new directory name"
2532 );
2533 panel.cancel(&menu::Cancel, window, cx);
2534 panel.update_visible_entries(None, false, false, window, cx);
2535 });
2536 cx.run_until_parked();
2537 assert_eq!(
2538 visible_entries_as_strings(&panel, 0..10, cx),
2539 &[
2540 //
2541 "v src <== selected",
2542 " > test"
2543 ],
2544 "File list should be unchanged after failed folder create confirmation"
2545 );
2546
2547 select_path(&panel, "src/test", cx);
2548 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2549 cx.executor().run_until_parked();
2550 assert_eq!(
2551 visible_entries_as_strings(&panel, 0..10, cx),
2552 &[
2553 //
2554 "v src",
2555 " > test <== selected"
2556 ]
2557 );
2558 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2559 cx.run_until_parked();
2560 panel.update_in(cx, |panel, window, cx| {
2561 assert!(panel.filename_editor.read(cx).is_focused(window));
2562 });
2563 assert_eq!(
2564 visible_entries_as_strings(&panel, 0..10, cx),
2565 &[
2566 "v src",
2567 " v test",
2568 " [EDITOR: ''] <== selected",
2569 " first.txt",
2570 " second.txt",
2571 " third.txt"
2572 ]
2573 );
2574 panel.update_in(cx, |panel, window, cx| {
2575 panel
2576 .filename_editor
2577 .update(cx, |editor, cx| editor.set_text("first.txt", window, cx));
2578 assert!(
2579 panel.confirm_edit(true, window, cx).is_none(),
2580 "Should not allow to confirm on conflicting new file name"
2581 );
2582 });
2583 cx.executor().run_until_parked();
2584 panel.update_in(cx, |panel, window, cx| {
2585 assert!(
2586 panel.state.edit_state.is_some(),
2587 "Edit state should not be None after conflicting new file name"
2588 );
2589 panel.cancel(&menu::Cancel, window, cx);
2590 panel.update_visible_entries(None, false, false, window, cx);
2591 });
2592 cx.run_until_parked();
2593 assert_eq!(
2594 visible_entries_as_strings(&panel, 0..10, cx),
2595 &[
2596 "v src",
2597 " v test <== selected",
2598 " first.txt",
2599 " second.txt",
2600 " third.txt"
2601 ],
2602 "File list should be unchanged after failed file create confirmation"
2603 );
2604
2605 select_path(&panel, "src/test/first.txt", cx);
2606 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2607 cx.executor().run_until_parked();
2608 assert_eq!(
2609 visible_entries_as_strings(&panel, 0..10, cx),
2610 &[
2611 "v src",
2612 " v test",
2613 " first.txt <== selected",
2614 " second.txt",
2615 " third.txt"
2616 ],
2617 );
2618 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2619 cx.executor().run_until_parked();
2620 panel.update_in(cx, |panel, window, cx| {
2621 assert!(panel.filename_editor.read(cx).is_focused(window));
2622 });
2623 assert_eq!(
2624 visible_entries_as_strings(&panel, 0..10, cx),
2625 &[
2626 "v src",
2627 " v test",
2628 " [EDITOR: 'first.txt'] <== selected",
2629 " second.txt",
2630 " third.txt"
2631 ]
2632 );
2633 panel.update_in(cx, |panel, window, cx| {
2634 panel
2635 .filename_editor
2636 .update(cx, |editor, cx| editor.set_text("second.txt", window, cx));
2637 assert!(
2638 panel.confirm_edit(true, window, cx).is_none(),
2639 "Should not allow to confirm on conflicting file rename"
2640 )
2641 });
2642 cx.executor().run_until_parked();
2643 panel.update_in(cx, |panel, window, cx| {
2644 assert!(
2645 panel.state.edit_state.is_some(),
2646 "Edit state should not be None after conflicting file rename"
2647 );
2648 panel.cancel(&menu::Cancel, window, cx);
2649 });
2650 assert_eq!(
2651 visible_entries_as_strings(&panel, 0..10, cx),
2652 &[
2653 "v src",
2654 " v test",
2655 " first.txt <== selected",
2656 " second.txt",
2657 " third.txt"
2658 ],
2659 "File list should be unchanged after failed rename confirmation"
2660 );
2661 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2662 cx.executor().run_until_parked();
2663 // Try to duplicate and check history
2664 panel.update_in(cx, |panel, window, cx| {
2665 panel.duplicate(&Duplicate, window, cx)
2666 });
2667 cx.executor().run_until_parked();
2668
2669 assert_eq!(
2670 visible_entries_as_strings(&panel, 0..10, cx),
2671 &[
2672 "v src",
2673 " v test",
2674 " first.txt",
2675 " [EDITOR: 'first copy.txt'] <== selected <== marked",
2676 " second.txt",
2677 " third.txt"
2678 ],
2679 );
2680
2681 let confirm = panel.update_in(cx, |panel, window, cx| {
2682 panel
2683 .filename_editor
2684 .update(cx, |editor, cx| editor.set_text("fourth.txt", window, cx));
2685 panel.confirm_edit(true, window, cx).unwrap()
2686 });
2687 confirm.await.unwrap();
2688 cx.executor().run_until_parked();
2689
2690 assert_eq!(
2691 visible_entries_as_strings(&panel, 0..10, cx),
2692 &[
2693 "v src",
2694 " v test",
2695 " first.txt",
2696 " fourth.txt <== selected",
2697 " second.txt",
2698 " third.txt"
2699 ],
2700 "File list should be different after rename confirmation"
2701 );
2702
2703 panel.update_in(cx, |panel, window, cx| {
2704 panel.update_visible_entries(None, false, false, window, cx);
2705 });
2706 cx.executor().run_until_parked();
2707
2708 select_path(&panel, "src/test/first.txt", cx);
2709 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2710 cx.executor().run_until_parked();
2711
2712 workspace
2713 .read_with(cx, |this, cx| {
2714 assert!(
2715 this.recent_navigation_history_iter(cx)
2716 .any(|(project_path, abs_path)| {
2717 project_path.path == Arc::from(rel_path("test/fourth.txt"))
2718 && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt")))
2719 })
2720 );
2721 })
2722 .unwrap();
2723}
2724
2725// NOTE: This test is skipped on Windows, because on Windows,
2726// when it triggers the lsp store it converts `/src/test/first.txt` into an uri
2727// but it fails with message `"/src\\test\\first.txt" is not parseable as an URI`
2728#[gpui::test]
2729#[cfg_attr(target_os = "windows", ignore)]
2730async fn test_rename_item_and_check_history(cx: &mut gpui::TestAppContext) {
2731 init_test_with_editor(cx);
2732
2733 let fs = FakeFs::new(cx.executor());
2734 fs.insert_tree(
2735 "/src",
2736 json!({
2737 "test": {
2738 "first.txt": "// First Txt file",
2739 "second.txt": "// Second Txt file",
2740 "third.txt": "// Third Txt file",
2741 }
2742 }),
2743 )
2744 .await;
2745
2746 let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2747 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2748 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2749 let panel = workspace
2750 .update(cx, |workspace, window, cx| {
2751 let panel = ProjectPanel::new(workspace, window, cx);
2752 workspace.add_panel(panel.clone(), window, cx);
2753 panel
2754 })
2755 .unwrap();
2756 cx.run_until_parked();
2757
2758 select_path(&panel, "src", cx);
2759 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2760 cx.executor().run_until_parked();
2761 assert_eq!(
2762 visible_entries_as_strings(&panel, 0..10, cx),
2763 &[
2764 //
2765 "v src <== selected",
2766 " > test"
2767 ]
2768 );
2769
2770 select_path(&panel, "src/test", cx);
2771 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
2772 cx.executor().run_until_parked();
2773 assert_eq!(
2774 visible_entries_as_strings(&panel, 0..10, cx),
2775 &[
2776 //
2777 "v src",
2778 " > test <== selected"
2779 ]
2780 );
2781 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
2782 cx.run_until_parked();
2783 panel.update_in(cx, |panel, window, cx| {
2784 assert!(panel.filename_editor.read(cx).is_focused(window));
2785 });
2786
2787 select_path(&panel, "src/test/first.txt", cx);
2788 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2789 cx.executor().run_until_parked();
2790
2791 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
2792 cx.executor().run_until_parked();
2793
2794 assert_eq!(
2795 visible_entries_as_strings(&panel, 0..10, cx),
2796 &[
2797 "v src",
2798 " v test",
2799 " [EDITOR: 'first.txt'] <== selected <== marked",
2800 " second.txt",
2801 " third.txt"
2802 ],
2803 );
2804
2805 let confirm = panel.update_in(cx, |panel, window, cx| {
2806 panel
2807 .filename_editor
2808 .update(cx, |editor, cx| editor.set_text("fourth.txt", window, cx));
2809 panel.confirm_edit(true, window, cx).unwrap()
2810 });
2811 confirm.await.unwrap();
2812 cx.executor().run_until_parked();
2813
2814 assert_eq!(
2815 visible_entries_as_strings(&panel, 0..10, cx),
2816 &[
2817 "v src",
2818 " v test",
2819 " fourth.txt <== selected",
2820 " second.txt",
2821 " third.txt"
2822 ],
2823 "File list should be different after rename confirmation"
2824 );
2825
2826 panel.update_in(cx, |panel, window, cx| {
2827 panel.update_visible_entries(None, false, false, window, cx);
2828 });
2829 cx.executor().run_until_parked();
2830
2831 select_path(&panel, "src/test/second.txt", cx);
2832 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
2833 cx.executor().run_until_parked();
2834
2835 workspace
2836 .read_with(cx, |this, cx| {
2837 assert!(
2838 this.recent_navigation_history_iter(cx)
2839 .any(|(project_path, abs_path)| {
2840 project_path.path == Arc::from(rel_path("test/fourth.txt"))
2841 && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt")))
2842 })
2843 );
2844 })
2845 .unwrap();
2846}
2847
2848#[gpui::test]
2849async fn test_select_git_entry(cx: &mut gpui::TestAppContext) {
2850 init_test_with_editor(cx);
2851
2852 let fs = FakeFs::new(cx.executor());
2853 fs.insert_tree(
2854 path!("/root"),
2855 json!({
2856 "tree1": {
2857 ".git": {},
2858 "dir1": {
2859 "modified1.txt": "1",
2860 "unmodified1.txt": "1",
2861 "modified2.txt": "1",
2862 },
2863 "dir2": {
2864 "modified3.txt": "1",
2865 "unmodified2.txt": "1",
2866 },
2867 "modified4.txt": "1",
2868 "unmodified3.txt": "1",
2869 },
2870 "tree2": {
2871 ".git": {},
2872 "dir3": {
2873 "modified5.txt": "1",
2874 "unmodified4.txt": "1",
2875 },
2876 "modified6.txt": "1",
2877 "unmodified5.txt": "1",
2878 }
2879 }),
2880 )
2881 .await;
2882
2883 // Mark files as git modified
2884 fs.set_head_and_index_for_repo(
2885 path!("/root/tree1/.git").as_ref(),
2886 &[
2887 ("dir1/modified1.txt", "modified".into()),
2888 ("dir1/modified2.txt", "modified".into()),
2889 ("modified4.txt", "modified".into()),
2890 ("dir2/modified3.txt", "modified".into()),
2891 ],
2892 );
2893 fs.set_head_and_index_for_repo(
2894 path!("/root/tree2/.git").as_ref(),
2895 &[
2896 ("dir3/modified5.txt", "modified".into()),
2897 ("modified6.txt", "modified".into()),
2898 ],
2899 );
2900
2901 let project = Project::test(
2902 fs.clone(),
2903 [path!("/root/tree1").as_ref(), path!("/root/tree2").as_ref()],
2904 cx,
2905 )
2906 .await;
2907
2908 let (scan1_complete, scan2_complete) = project.update(cx, |project, cx| {
2909 let mut worktrees = project.worktrees(cx);
2910 let worktree1 = worktrees.next().unwrap();
2911 let worktree2 = worktrees.next().unwrap();
2912 (
2913 worktree1.read(cx).as_local().unwrap().scan_complete(),
2914 worktree2.read(cx).as_local().unwrap().scan_complete(),
2915 )
2916 });
2917 scan1_complete.await;
2918 scan2_complete.await;
2919 cx.run_until_parked();
2920
2921 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2922 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2923 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
2924 cx.run_until_parked();
2925
2926 // Check initial state
2927 assert_eq!(
2928 visible_entries_as_strings(&panel, 0..15, cx),
2929 &[
2930 "v tree1",
2931 " > .git",
2932 " > dir1",
2933 " > dir2",
2934 " modified4.txt",
2935 " unmodified3.txt",
2936 "v tree2",
2937 " > .git",
2938 " > dir3",
2939 " modified6.txt",
2940 " unmodified5.txt"
2941 ],
2942 );
2943
2944 // Test selecting next modified entry
2945 panel.update_in(cx, |panel, window, cx| {
2946 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2947 });
2948 cx.run_until_parked();
2949
2950 assert_eq!(
2951 visible_entries_as_strings(&panel, 0..6, cx),
2952 &[
2953 "v tree1",
2954 " > .git",
2955 " v dir1",
2956 " modified1.txt <== selected",
2957 " modified2.txt",
2958 " unmodified1.txt",
2959 ],
2960 );
2961
2962 panel.update_in(cx, |panel, window, cx| {
2963 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2964 });
2965 cx.run_until_parked();
2966
2967 assert_eq!(
2968 visible_entries_as_strings(&panel, 0..6, cx),
2969 &[
2970 "v tree1",
2971 " > .git",
2972 " v dir1",
2973 " modified1.txt",
2974 " modified2.txt <== selected",
2975 " unmodified1.txt",
2976 ],
2977 );
2978
2979 panel.update_in(cx, |panel, window, cx| {
2980 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2981 });
2982 cx.run_until_parked();
2983
2984 assert_eq!(
2985 visible_entries_as_strings(&panel, 6..9, cx),
2986 &[
2987 " v dir2",
2988 " modified3.txt <== selected",
2989 " unmodified2.txt",
2990 ],
2991 );
2992
2993 panel.update_in(cx, |panel, window, cx| {
2994 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
2995 });
2996 cx.run_until_parked();
2997
2998 assert_eq!(
2999 visible_entries_as_strings(&panel, 9..11, cx),
3000 &[" modified4.txt <== selected", " unmodified3.txt",],
3001 );
3002
3003 panel.update_in(cx, |panel, window, cx| {
3004 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
3005 });
3006 cx.run_until_parked();
3007
3008 assert_eq!(
3009 visible_entries_as_strings(&panel, 13..16, cx),
3010 &[
3011 " v dir3",
3012 " modified5.txt <== selected",
3013 " unmodified4.txt",
3014 ],
3015 );
3016
3017 panel.update_in(cx, |panel, window, cx| {
3018 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
3019 });
3020 cx.run_until_parked();
3021
3022 assert_eq!(
3023 visible_entries_as_strings(&panel, 16..18, cx),
3024 &[" modified6.txt <== selected", " unmodified5.txt",],
3025 );
3026
3027 // Wraps around to first modified file
3028 panel.update_in(cx, |panel, window, cx| {
3029 panel.select_next_git_entry(&SelectNextGitEntry, window, cx);
3030 });
3031 cx.run_until_parked();
3032
3033 assert_eq!(
3034 visible_entries_as_strings(&panel, 0..18, cx),
3035 &[
3036 "v tree1",
3037 " > .git",
3038 " v dir1",
3039 " modified1.txt <== selected",
3040 " modified2.txt",
3041 " unmodified1.txt",
3042 " v dir2",
3043 " modified3.txt",
3044 " unmodified2.txt",
3045 " modified4.txt",
3046 " unmodified3.txt",
3047 "v tree2",
3048 " > .git",
3049 " v dir3",
3050 " modified5.txt",
3051 " unmodified4.txt",
3052 " modified6.txt",
3053 " unmodified5.txt",
3054 ],
3055 );
3056
3057 // Wraps around again to last modified file
3058 panel.update_in(cx, |panel, window, cx| {
3059 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3060 });
3061 cx.run_until_parked();
3062
3063 assert_eq!(
3064 visible_entries_as_strings(&panel, 16..18, cx),
3065 &[" modified6.txt <== selected", " unmodified5.txt",],
3066 );
3067
3068 panel.update_in(cx, |panel, window, cx| {
3069 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3070 });
3071 cx.run_until_parked();
3072
3073 assert_eq!(
3074 visible_entries_as_strings(&panel, 13..16, cx),
3075 &[
3076 " v dir3",
3077 " modified5.txt <== selected",
3078 " unmodified4.txt",
3079 ],
3080 );
3081
3082 panel.update_in(cx, |panel, window, cx| {
3083 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3084 });
3085 cx.run_until_parked();
3086
3087 assert_eq!(
3088 visible_entries_as_strings(&panel, 9..11, cx),
3089 &[" modified4.txt <== selected", " unmodified3.txt",],
3090 );
3091
3092 panel.update_in(cx, |panel, window, cx| {
3093 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3094 });
3095 cx.run_until_parked();
3096
3097 assert_eq!(
3098 visible_entries_as_strings(&panel, 6..9, cx),
3099 &[
3100 " v dir2",
3101 " modified3.txt <== selected",
3102 " unmodified2.txt",
3103 ],
3104 );
3105
3106 panel.update_in(cx, |panel, window, cx| {
3107 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3108 });
3109 cx.run_until_parked();
3110
3111 assert_eq!(
3112 visible_entries_as_strings(&panel, 0..6, cx),
3113 &[
3114 "v tree1",
3115 " > .git",
3116 " v dir1",
3117 " modified1.txt",
3118 " modified2.txt <== selected",
3119 " unmodified1.txt",
3120 ],
3121 );
3122
3123 panel.update_in(cx, |panel, window, cx| {
3124 panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx);
3125 });
3126 cx.run_until_parked();
3127
3128 assert_eq!(
3129 visible_entries_as_strings(&panel, 0..6, cx),
3130 &[
3131 "v tree1",
3132 " > .git",
3133 " v dir1",
3134 " modified1.txt <== selected",
3135 " modified2.txt",
3136 " unmodified1.txt",
3137 ],
3138 );
3139}
3140
3141#[gpui::test]
3142async fn test_select_directory(cx: &mut gpui::TestAppContext) {
3143 init_test_with_editor(cx);
3144
3145 let fs = FakeFs::new(cx.executor());
3146 fs.insert_tree(
3147 "/project_root",
3148 json!({
3149 "dir_1": {
3150 "nested_dir": {
3151 "file_a.py": "# File contents",
3152 }
3153 },
3154 "file_1.py": "# File contents",
3155 "dir_2": {
3156
3157 },
3158 "dir_3": {
3159
3160 },
3161 "file_2.py": "# File contents",
3162 "dir_4": {
3163
3164 },
3165 }),
3166 )
3167 .await;
3168
3169 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3170 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3171 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3172 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3173 cx.run_until_parked();
3174
3175 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3176 cx.executor().run_until_parked();
3177 select_path(&panel, "project_root/dir_1", cx);
3178 cx.executor().run_until_parked();
3179 assert_eq!(
3180 visible_entries_as_strings(&panel, 0..10, cx),
3181 &[
3182 "v project_root",
3183 " > dir_1 <== selected",
3184 " > dir_2",
3185 " > dir_3",
3186 " > dir_4",
3187 " file_1.py",
3188 " file_2.py",
3189 ]
3190 );
3191 panel.update_in(cx, |panel, window, cx| {
3192 panel.select_prev_directory(&SelectPrevDirectory, window, cx)
3193 });
3194
3195 assert_eq!(
3196 visible_entries_as_strings(&panel, 0..10, cx),
3197 &[
3198 "v project_root <== selected",
3199 " > dir_1",
3200 " > dir_2",
3201 " > dir_3",
3202 " > dir_4",
3203 " file_1.py",
3204 " file_2.py",
3205 ]
3206 );
3207
3208 panel.update_in(cx, |panel, window, cx| {
3209 panel.select_prev_directory(&SelectPrevDirectory, window, cx)
3210 });
3211
3212 assert_eq!(
3213 visible_entries_as_strings(&panel, 0..10, cx),
3214 &[
3215 "v project_root",
3216 " > dir_1",
3217 " > dir_2",
3218 " > dir_3",
3219 " > dir_4 <== selected",
3220 " file_1.py",
3221 " file_2.py",
3222 ]
3223 );
3224
3225 panel.update_in(cx, |panel, window, cx| {
3226 panel.select_next_directory(&SelectNextDirectory, window, cx)
3227 });
3228
3229 assert_eq!(
3230 visible_entries_as_strings(&panel, 0..10, cx),
3231 &[
3232 "v project_root <== selected",
3233 " > dir_1",
3234 " > dir_2",
3235 " > dir_3",
3236 " > dir_4",
3237 " file_1.py",
3238 " file_2.py",
3239 ]
3240 );
3241}
3242
3243#[gpui::test]
3244async fn test_select_first_last(cx: &mut gpui::TestAppContext) {
3245 init_test_with_editor(cx);
3246
3247 let fs = FakeFs::new(cx.executor());
3248 fs.insert_tree(
3249 "/project_root",
3250 json!({
3251 "dir_1": {
3252 "nested_dir": {
3253 "file_a.py": "# File contents",
3254 }
3255 },
3256 "file_1.py": "# File contents",
3257 "file_2.py": "# File contents",
3258 "zdir_2": {
3259 "nested_dir2": {
3260 "file_b.py": "# File contents",
3261 }
3262 },
3263 }),
3264 )
3265 .await;
3266
3267 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3268 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3269 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3270 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3271 cx.run_until_parked();
3272
3273 assert_eq!(
3274 visible_entries_as_strings(&panel, 0..10, cx),
3275 &[
3276 "v project_root",
3277 " > dir_1",
3278 " > zdir_2",
3279 " file_1.py",
3280 " file_2.py",
3281 ]
3282 );
3283 panel.update_in(cx, |panel, window, cx| {
3284 panel.select_first(&SelectFirst, window, cx)
3285 });
3286
3287 assert_eq!(
3288 visible_entries_as_strings(&panel, 0..10, cx),
3289 &[
3290 "v project_root <== selected",
3291 " > dir_1",
3292 " > zdir_2",
3293 " file_1.py",
3294 " file_2.py",
3295 ]
3296 );
3297
3298 panel.update_in(cx, |panel, window, cx| {
3299 panel.select_last(&SelectLast, window, cx)
3300 });
3301
3302 assert_eq!(
3303 visible_entries_as_strings(&panel, 0..10, cx),
3304 &[
3305 "v project_root",
3306 " > dir_1",
3307 " > zdir_2",
3308 " file_1.py",
3309 " file_2.py <== selected",
3310 ]
3311 );
3312
3313 cx.update(|_, cx| {
3314 let settings = *ProjectPanelSettings::get_global(cx);
3315 ProjectPanelSettings::override_global(
3316 ProjectPanelSettings {
3317 hide_root: true,
3318 ..settings
3319 },
3320 cx,
3321 );
3322 });
3323
3324 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3325 cx.run_until_parked();
3326
3327 #[rustfmt::skip]
3328 assert_eq!(
3329 visible_entries_as_strings(&panel, 0..10, cx),
3330 &[
3331 "> dir_1",
3332 "> zdir_2",
3333 " file_1.py",
3334 " file_2.py",
3335 ],
3336 "With hide_root=true, root should be hidden"
3337 );
3338
3339 panel.update_in(cx, |panel, window, cx| {
3340 panel.select_first(&SelectFirst, window, cx)
3341 });
3342
3343 assert_eq!(
3344 visible_entries_as_strings(&panel, 0..10, cx),
3345 &[
3346 "> dir_1 <== selected",
3347 "> zdir_2",
3348 " file_1.py",
3349 " file_2.py",
3350 ],
3351 "With hide_root=true, first entry should be dir_1, not the hidden root"
3352 );
3353}
3354
3355#[gpui::test]
3356async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) {
3357 init_test_with_editor(cx);
3358
3359 let fs = FakeFs::new(cx.executor());
3360 fs.insert_tree(
3361 "/project_root",
3362 json!({
3363 "dir_1": {
3364 "nested_dir": {
3365 "file_a.py": "# File contents",
3366 }
3367 },
3368 "file_1.py": "# File contents",
3369 }),
3370 )
3371 .await;
3372
3373 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3374 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3375 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3376 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3377 cx.run_until_parked();
3378
3379 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3380 cx.executor().run_until_parked();
3381 select_path(&panel, "project_root/dir_1", cx);
3382 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3383 select_path(&panel, "project_root/dir_1/nested_dir", cx);
3384 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3385 panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
3386 cx.executor().run_until_parked();
3387 assert_eq!(
3388 visible_entries_as_strings(&panel, 0..10, cx),
3389 &[
3390 "v project_root",
3391 " v dir_1",
3392 " > nested_dir <== selected",
3393 " file_1.py",
3394 ]
3395 );
3396}
3397
3398#[gpui::test]
3399async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
3400 init_test_with_editor(cx);
3401
3402 let fs = FakeFs::new(cx.executor());
3403 fs.insert_tree(
3404 "/project_root",
3405 json!({
3406 "dir_1": {
3407 "nested_dir": {
3408 "file_a.py": "# File contents",
3409 "file_b.py": "# File contents",
3410 "file_c.py": "# File contents",
3411 },
3412 "file_1.py": "# File contents",
3413 "file_2.py": "# File contents",
3414 "file_3.py": "# File contents",
3415 },
3416 "dir_2": {
3417 "file_1.py": "# File contents",
3418 "file_2.py": "# File contents",
3419 "file_3.py": "# File contents",
3420 }
3421 }),
3422 )
3423 .await;
3424
3425 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3426 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3427 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3428 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3429 cx.run_until_parked();
3430
3431 panel.update_in(cx, |panel, window, cx| {
3432 panel.collapse_all_entries(&CollapseAllEntries, window, cx)
3433 });
3434 cx.executor().run_until_parked();
3435 assert_eq!(
3436 visible_entries_as_strings(&panel, 0..10, cx),
3437 &["v project_root", " > dir_1", " > dir_2",]
3438 );
3439
3440 // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
3441 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3442 cx.executor().run_until_parked();
3443 assert_eq!(
3444 visible_entries_as_strings(&panel, 0..10, cx),
3445 &[
3446 "v project_root",
3447 " v dir_1 <== selected",
3448 " > nested_dir",
3449 " file_1.py",
3450 " file_2.py",
3451 " file_3.py",
3452 " > dir_2",
3453 ]
3454 );
3455}
3456
3457#[gpui::test]
3458async fn test_collapse_all_entries_multiple_worktrees(cx: &mut gpui::TestAppContext) {
3459 init_test_with_editor(cx);
3460
3461 let fs = FakeFs::new(cx.executor());
3462 let worktree_content = json!({
3463 "dir_1": {
3464 "file_1.py": "# File contents",
3465 },
3466 "dir_2": {
3467 "file_1.py": "# File contents",
3468 }
3469 });
3470
3471 fs.insert_tree("/project_root_1", worktree_content.clone())
3472 .await;
3473 fs.insert_tree("/project_root_2", worktree_content).await;
3474
3475 let project = Project::test(
3476 fs.clone(),
3477 ["/project_root_1".as_ref(), "/project_root_2".as_ref()],
3478 cx,
3479 )
3480 .await;
3481 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3482 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3483 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3484 cx.run_until_parked();
3485
3486 panel.update_in(cx, |panel, window, cx| {
3487 panel.collapse_all_entries(&CollapseAllEntries, window, cx)
3488 });
3489 cx.executor().run_until_parked();
3490 assert_eq!(
3491 visible_entries_as_strings(&panel, 0..10, cx),
3492 &["> project_root_1", "> project_root_2",]
3493 );
3494}
3495
3496#[gpui::test]
3497async fn test_collapse_all_entries_with_collapsed_root(cx: &mut gpui::TestAppContext) {
3498 init_test_with_editor(cx);
3499
3500 let fs = FakeFs::new(cx.executor());
3501 fs.insert_tree(
3502 "/project_root",
3503 json!({
3504 "dir_1": {
3505 "nested_dir": {
3506 "file_a.py": "# File contents",
3507 "file_b.py": "# File contents",
3508 "file_c.py": "# File contents",
3509 },
3510 "file_1.py": "# File contents",
3511 "file_2.py": "# File contents",
3512 "file_3.py": "# File contents",
3513 },
3514 "dir_2": {
3515 "file_1.py": "# File contents",
3516 "file_2.py": "# File contents",
3517 "file_3.py": "# File contents",
3518 }
3519 }),
3520 )
3521 .await;
3522
3523 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3524 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3525 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3526 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3527 cx.run_until_parked();
3528
3529 // Open project_root/dir_1 to ensure that a nested directory is expanded
3530 toggle_expand_dir(&panel, "project_root/dir_1", cx);
3531 cx.executor().run_until_parked();
3532 assert_eq!(
3533 visible_entries_as_strings(&panel, 0..10, cx),
3534 &[
3535 "v project_root",
3536 " v dir_1 <== selected",
3537 " > nested_dir",
3538 " file_1.py",
3539 " file_2.py",
3540 " file_3.py",
3541 " > dir_2",
3542 ]
3543 );
3544
3545 // Close root directory
3546 toggle_expand_dir(&panel, "project_root", cx);
3547 cx.executor().run_until_parked();
3548 assert_eq!(
3549 visible_entries_as_strings(&panel, 0..10, cx),
3550 &["> project_root <== selected"]
3551 );
3552
3553 // Run collapse_all_entries and make sure root is not expanded
3554 panel.update_in(cx, |panel, window, cx| {
3555 panel.collapse_all_entries(&CollapseAllEntries, window, cx)
3556 });
3557 cx.executor().run_until_parked();
3558 assert_eq!(
3559 visible_entries_as_strings(&panel, 0..10, cx),
3560 &["> project_root <== selected"]
3561 );
3562}
3563
3564#[gpui::test]
3565async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
3566 init_test(cx);
3567
3568 let fs = FakeFs::new(cx.executor());
3569 fs.as_fake().insert_tree(path!("/root"), json!({})).await;
3570 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
3571 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3572 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3573 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3574 cx.run_until_parked();
3575
3576 // Make a new buffer with no backing file
3577 workspace
3578 .update(cx, |workspace, window, cx| {
3579 Editor::new_file(workspace, &Default::default(), window, cx)
3580 })
3581 .unwrap();
3582
3583 cx.executor().run_until_parked();
3584
3585 // "Save as" the buffer, creating a new backing file for it
3586 let save_task = workspace
3587 .update(cx, |workspace, window, cx| {
3588 workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
3589 })
3590 .unwrap();
3591
3592 cx.executor().run_until_parked();
3593 cx.simulate_new_path_selection(|_| Some(PathBuf::from(path!("/root/new"))));
3594 save_task.await.unwrap();
3595
3596 // Rename the file
3597 select_path(&panel, "root/new", cx);
3598 assert_eq!(
3599 visible_entries_as_strings(&panel, 0..10, cx),
3600 &["v root", " new <== selected <== marked"]
3601 );
3602 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
3603 panel.update_in(cx, |panel, window, cx| {
3604 panel
3605 .filename_editor
3606 .update(cx, |editor, cx| editor.set_text("newer", window, cx));
3607 });
3608 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
3609
3610 cx.executor().run_until_parked();
3611 assert_eq!(
3612 visible_entries_as_strings(&panel, 0..10, cx),
3613 &["v root", " newer <== selected"]
3614 );
3615
3616 workspace
3617 .update(cx, |workspace, window, cx| {
3618 workspace.save_active_item(workspace::SaveIntent::Save, window, cx)
3619 })
3620 .unwrap()
3621 .await
3622 .unwrap();
3623
3624 cx.executor().run_until_parked();
3625 // assert that saving the file doesn't restore "new"
3626 assert_eq!(
3627 visible_entries_as_strings(&panel, 0..10, cx),
3628 &["v root", " newer <== selected"]
3629 );
3630}
3631
3632// NOTE: This test is skipped on Windows, because on Windows, unlike on Unix,
3633// you can't rename a directory which some program has already open. This is a
3634// limitation of the Windows. Since Zed will have the root open, it will hold an open handle
3635// to it, and thus renaming it will fail on Windows.
3636// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
3637// See: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_rename_information
3638#[gpui::test]
3639#[cfg_attr(target_os = "windows", ignore)]
3640async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
3641 init_test_with_editor(cx);
3642
3643 let fs = FakeFs::new(cx.executor());
3644 fs.insert_tree(
3645 "/root1",
3646 json!({
3647 "dir1": {
3648 "file1.txt": "content 1",
3649 },
3650 }),
3651 )
3652 .await;
3653
3654 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
3655 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3656 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3657 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3658 cx.run_until_parked();
3659
3660 toggle_expand_dir(&panel, "root1/dir1", cx);
3661
3662 assert_eq!(
3663 visible_entries_as_strings(&panel, 0..20, cx),
3664 &["v root1", " v dir1 <== selected", " file1.txt",],
3665 "Initial state with worktrees"
3666 );
3667
3668 select_path(&panel, "root1", cx);
3669 assert_eq!(
3670 visible_entries_as_strings(&panel, 0..20, cx),
3671 &["v root1 <== selected", " v dir1", " file1.txt",],
3672 );
3673
3674 // Rename root1 to new_root1
3675 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
3676
3677 assert_eq!(
3678 visible_entries_as_strings(&panel, 0..20, cx),
3679 &[
3680 "v [EDITOR: 'root1'] <== selected",
3681 " v dir1",
3682 " file1.txt",
3683 ],
3684 );
3685
3686 let confirm = panel.update_in(cx, |panel, window, cx| {
3687 panel
3688 .filename_editor
3689 .update(cx, |editor, cx| editor.set_text("new_root1", window, cx));
3690 panel.confirm_edit(true, window, cx).unwrap()
3691 });
3692 confirm.await.unwrap();
3693 cx.run_until_parked();
3694 assert_eq!(
3695 visible_entries_as_strings(&panel, 0..20, cx),
3696 &[
3697 "v new_root1 <== selected",
3698 " v dir1",
3699 " file1.txt",
3700 ],
3701 "Should update worktree name"
3702 );
3703
3704 // Ensure internal paths have been updated
3705 select_path(&panel, "new_root1/dir1/file1.txt", cx);
3706 assert_eq!(
3707 visible_entries_as_strings(&panel, 0..20, cx),
3708 &[
3709 "v new_root1",
3710 " v dir1",
3711 " file1.txt <== selected",
3712 ],
3713 "Files in renamed worktree are selectable"
3714 );
3715}
3716
3717#[gpui::test]
3718async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) {
3719 init_test_with_editor(cx);
3720
3721 let fs = FakeFs::new(cx.executor());
3722 fs.insert_tree(
3723 "/root1",
3724 json!({
3725 "dir1": { "file1.txt": "content" },
3726 "file2.txt": "content",
3727 }),
3728 )
3729 .await;
3730 fs.insert_tree("/root2", json!({ "file3.txt": "content" }))
3731 .await;
3732
3733 // Test 1: Single worktree, hide_root=true - rename should be blocked
3734 {
3735 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
3736 let workspace =
3737 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3738 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3739
3740 cx.update(|_, cx| {
3741 let settings = *ProjectPanelSettings::get_global(cx);
3742 ProjectPanelSettings::override_global(
3743 ProjectPanelSettings {
3744 hide_root: true,
3745 ..settings
3746 },
3747 cx,
3748 );
3749 });
3750
3751 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3752 cx.run_until_parked();
3753
3754 panel.update(cx, |panel, cx| {
3755 let project = panel.project.read(cx);
3756 let worktree = project.visible_worktrees(cx).next().unwrap();
3757 let root_entry = worktree.read(cx).root_entry().unwrap();
3758 panel.state.selection = Some(SelectedEntry {
3759 worktree_id: worktree.read(cx).id(),
3760 entry_id: root_entry.id,
3761 });
3762 });
3763
3764 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
3765
3766 assert!(
3767 panel.read_with(cx, |panel, _| panel.state.edit_state.is_none()),
3768 "Rename should be blocked when hide_root=true with single worktree"
3769 );
3770 }
3771
3772 // Test 2: Multiple worktrees, hide_root=true - rename should work
3773 {
3774 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
3775 let workspace =
3776 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3777 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3778
3779 cx.update(|_, cx| {
3780 let settings = *ProjectPanelSettings::get_global(cx);
3781 ProjectPanelSettings::override_global(
3782 ProjectPanelSettings {
3783 hide_root: true,
3784 ..settings
3785 },
3786 cx,
3787 );
3788 });
3789
3790 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3791 cx.run_until_parked();
3792
3793 select_path(&panel, "root1", cx);
3794 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
3795
3796 #[cfg(target_os = "windows")]
3797 assert!(
3798 panel.read_with(cx, |panel, _| panel.state.edit_state.is_none()),
3799 "Rename should be blocked on Windows even with multiple worktrees"
3800 );
3801
3802 #[cfg(not(target_os = "windows"))]
3803 {
3804 assert!(
3805 panel.read_with(cx, |panel, _| panel.state.edit_state.is_some()),
3806 "Rename should work with multiple worktrees on non-Windows when hide_root=true"
3807 );
3808 panel.update_in(cx, |panel, window, cx| {
3809 panel.cancel(&menu::Cancel, window, cx)
3810 });
3811 }
3812 }
3813}
3814
3815#[gpui::test]
3816async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
3817 init_test_with_editor(cx);
3818 let fs = FakeFs::new(cx.executor());
3819 fs.insert_tree(
3820 "/project_root",
3821 json!({
3822 "dir_1": {
3823 "nested_dir": {
3824 "file_a.py": "# File contents",
3825 }
3826 },
3827 "file_1.py": "# File contents",
3828 }),
3829 )
3830 .await;
3831
3832 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
3833 let worktree_id = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
3834 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3835 let cx = &mut VisualTestContext::from_window(*workspace, cx);
3836 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3837 cx.run_until_parked();
3838
3839 cx.update(|window, cx| {
3840 panel.update(cx, |this, cx| {
3841 this.select_next(&Default::default(), window, cx);
3842 this.expand_selected_entry(&Default::default(), window, cx);
3843 })
3844 });
3845 cx.run_until_parked();
3846
3847 cx.update(|window, cx| {
3848 panel.update(cx, |this, cx| {
3849 this.expand_selected_entry(&Default::default(), window, cx);
3850 })
3851 });
3852 cx.run_until_parked();
3853
3854 cx.update(|window, cx| {
3855 panel.update(cx, |this, cx| {
3856 this.select_next(&Default::default(), window, cx);
3857 this.expand_selected_entry(&Default::default(), window, cx);
3858 })
3859 });
3860 cx.run_until_parked();
3861
3862 cx.update(|window, cx| {
3863 panel.update(cx, |this, cx| {
3864 this.select_next(&Default::default(), window, cx);
3865 })
3866 });
3867 cx.run_until_parked();
3868
3869 assert_eq!(
3870 visible_entries_as_strings(&panel, 0..10, cx),
3871 &[
3872 "v project_root",
3873 " v dir_1",
3874 " v nested_dir",
3875 " file_a.py <== selected",
3876 " file_1.py",
3877 ]
3878 );
3879 let modifiers_with_shift = gpui::Modifiers {
3880 shift: true,
3881 ..Default::default()
3882 };
3883 cx.run_until_parked();
3884 cx.simulate_modifiers_change(modifiers_with_shift);
3885 cx.update(|window, cx| {
3886 panel.update(cx, |this, cx| {
3887 this.select_next(&Default::default(), window, cx);
3888 })
3889 });
3890 assert_eq!(
3891 visible_entries_as_strings(&panel, 0..10, cx),
3892 &[
3893 "v project_root",
3894 " v dir_1",
3895 " v nested_dir",
3896 " file_a.py",
3897 " file_1.py <== selected <== marked",
3898 ]
3899 );
3900 cx.update(|window, cx| {
3901 panel.update(cx, |this, cx| {
3902 this.select_previous(&Default::default(), window, cx);
3903 })
3904 });
3905 assert_eq!(
3906 visible_entries_as_strings(&panel, 0..10, cx),
3907 &[
3908 "v project_root",
3909 " v dir_1",
3910 " v nested_dir",
3911 " file_a.py <== selected <== marked",
3912 " file_1.py <== marked",
3913 ]
3914 );
3915 cx.update(|window, cx| {
3916 panel.update(cx, |this, cx| {
3917 let drag = DraggedSelection {
3918 active_selection: this.state.selection.unwrap(),
3919 marked_selections: this.marked_entries.clone().into(),
3920 };
3921 let target_entry = this
3922 .project
3923 .read(cx)
3924 .entry_for_path(&(worktree_id, rel_path("")).into(), cx)
3925 .unwrap();
3926 this.drag_onto(&drag, target_entry.id, false, window, cx);
3927 });
3928 });
3929 cx.run_until_parked();
3930 assert_eq!(
3931 visible_entries_as_strings(&panel, 0..10, cx),
3932 &[
3933 "v project_root",
3934 " v dir_1",
3935 " v nested_dir",
3936 " file_1.py <== marked",
3937 " file_a.py <== selected <== marked",
3938 ]
3939 );
3940 // ESC clears out all marks
3941 cx.update(|window, cx| {
3942 panel.update(cx, |this, cx| {
3943 this.cancel(&menu::Cancel, window, cx);
3944 })
3945 });
3946 assert_eq!(
3947 visible_entries_as_strings(&panel, 0..10, cx),
3948 &[
3949 "v project_root",
3950 " v dir_1",
3951 " v nested_dir",
3952 " file_1.py",
3953 " file_a.py <== selected",
3954 ]
3955 );
3956 // ESC clears out all marks
3957 cx.update(|window, cx| {
3958 panel.update(cx, |this, cx| {
3959 this.select_previous(&SelectPrevious, window, cx);
3960 this.select_next(&SelectNext, window, cx);
3961 })
3962 });
3963 assert_eq!(
3964 visible_entries_as_strings(&panel, 0..10, cx),
3965 &[
3966 "v project_root",
3967 " v dir_1",
3968 " v nested_dir",
3969 " file_1.py <== marked",
3970 " file_a.py <== selected <== marked",
3971 ]
3972 );
3973 cx.simulate_modifiers_change(Default::default());
3974 cx.update(|window, cx| {
3975 panel.update(cx, |this, cx| {
3976 this.cut(&Cut, window, cx);
3977 this.select_previous(&SelectPrevious, window, cx);
3978 this.select_previous(&SelectPrevious, window, cx);
3979
3980 this.paste(&Paste, window, cx);
3981 this.update_visible_entries(None, false, false, window, cx);
3982 })
3983 });
3984 cx.run_until_parked();
3985 assert_eq!(
3986 visible_entries_as_strings(&panel, 0..10, cx),
3987 &[
3988 "v project_root",
3989 " v dir_1",
3990 " v nested_dir",
3991 " file_1.py <== marked",
3992 " file_a.py <== selected <== marked",
3993 ]
3994 );
3995 cx.simulate_modifiers_change(modifiers_with_shift);
3996 cx.update(|window, cx| {
3997 panel.update(cx, |this, cx| {
3998 this.expand_selected_entry(&Default::default(), window, cx);
3999 this.select_next(&SelectNext, window, cx);
4000 this.select_next(&SelectNext, window, cx);
4001 })
4002 });
4003 submit_deletion(&panel, cx);
4004 assert_eq!(
4005 visible_entries_as_strings(&panel, 0..10, cx),
4006 &[
4007 "v project_root",
4008 " v dir_1",
4009 " v nested_dir <== selected",
4010 ]
4011 );
4012}
4013
4014#[gpui::test]
4015async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
4016 init_test(cx);
4017
4018 let fs = FakeFs::new(cx.executor());
4019 fs.insert_tree(
4020 "/root",
4021 json!({
4022 "a": {
4023 "b": {
4024 "c": {
4025 "d": {}
4026 }
4027 }
4028 },
4029 "target_destination": {}
4030 }),
4031 )
4032 .await;
4033
4034 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4035 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4036 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4037
4038 cx.update(|_, cx| {
4039 let settings = *ProjectPanelSettings::get_global(cx);
4040 ProjectPanelSettings::override_global(
4041 ProjectPanelSettings {
4042 auto_fold_dirs: true,
4043 ..settings
4044 },
4045 cx,
4046 );
4047 });
4048
4049 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4050 cx.run_until_parked();
4051
4052 // Case 1: Move last dir 'd' - should move only 'd', leaving 'a/b/c'
4053 select_path(&panel, "root/a/b/c/d", cx);
4054 panel.update_in(cx, |panel, window, cx| {
4055 let drag = DraggedSelection {
4056 active_selection: SelectedEntry {
4057 worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
4058 entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
4059 },
4060 marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
4061 };
4062 let target_entry = panel
4063 .project
4064 .read(cx)
4065 .visible_worktrees(cx)
4066 .next()
4067 .unwrap()
4068 .read(cx)
4069 .entry_for_path(rel_path("target_destination"))
4070 .unwrap();
4071 panel.drag_onto(&drag, target_entry.id, false, window, cx);
4072 });
4073 cx.executor().run_until_parked();
4074
4075 assert_eq!(
4076 visible_entries_as_strings(&panel, 0..10, cx),
4077 &[
4078 "v root",
4079 " > a/b/c",
4080 " > target_destination/d <== selected"
4081 ],
4082 "Moving last empty directory 'd' should leave 'a/b/c' and move only 'd'"
4083 );
4084
4085 // Reset
4086 select_path(&panel, "root/target_destination/d", cx);
4087 panel.update_in(cx, |panel, window, cx| {
4088 let drag = DraggedSelection {
4089 active_selection: SelectedEntry {
4090 worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
4091 entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
4092 },
4093 marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
4094 };
4095 let target_entry = panel
4096 .project
4097 .read(cx)
4098 .visible_worktrees(cx)
4099 .next()
4100 .unwrap()
4101 .read(cx)
4102 .entry_for_path(rel_path("a/b/c"))
4103 .unwrap();
4104 panel.drag_onto(&drag, target_entry.id, false, window, cx);
4105 });
4106 cx.executor().run_until_parked();
4107
4108 // Case 2: Move middle dir 'b' - should move 'b/c/d', leaving only 'a'
4109 select_path(&panel, "root/a/b", cx);
4110 panel.update_in(cx, |panel, window, cx| {
4111 let drag = DraggedSelection {
4112 active_selection: SelectedEntry {
4113 worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
4114 entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
4115 },
4116 marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
4117 };
4118 let target_entry = panel
4119 .project
4120 .read(cx)
4121 .visible_worktrees(cx)
4122 .next()
4123 .unwrap()
4124 .read(cx)
4125 .entry_for_path(rel_path("target_destination"))
4126 .unwrap();
4127 panel.drag_onto(&drag, target_entry.id, false, window, cx);
4128 });
4129 cx.executor().run_until_parked();
4130
4131 assert_eq!(
4132 visible_entries_as_strings(&panel, 0..10, cx),
4133 &["v root", " v a", " > target_destination/b/c/d"],
4134 "Moving middle directory 'b' should leave only 'a' and move 'b/c/d'"
4135 );
4136
4137 // Reset
4138 select_path(&panel, "root/target_destination/b", cx);
4139 panel.update_in(cx, |panel, window, cx| {
4140 let drag = DraggedSelection {
4141 active_selection: SelectedEntry {
4142 worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
4143 entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
4144 },
4145 marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
4146 };
4147 let target_entry = panel
4148 .project
4149 .read(cx)
4150 .visible_worktrees(cx)
4151 .next()
4152 .unwrap()
4153 .read(cx)
4154 .entry_for_path(rel_path("a"))
4155 .unwrap();
4156 panel.drag_onto(&drag, target_entry.id, false, window, cx);
4157 });
4158 cx.executor().run_until_parked();
4159
4160 // Case 3: Move first dir 'a' - should move whole 'a/b/c/d'
4161 select_path(&panel, "root/a", cx);
4162 panel.update_in(cx, |panel, window, cx| {
4163 let drag = DraggedSelection {
4164 active_selection: SelectedEntry {
4165 worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
4166 entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
4167 },
4168 marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
4169 };
4170 let target_entry = panel
4171 .project
4172 .read(cx)
4173 .visible_worktrees(cx)
4174 .next()
4175 .unwrap()
4176 .read(cx)
4177 .entry_for_path(rel_path("target_destination"))
4178 .unwrap();
4179 panel.drag_onto(&drag, target_entry.id, false, window, cx);
4180 });
4181 cx.executor().run_until_parked();
4182
4183 assert_eq!(
4184 visible_entries_as_strings(&panel, 0..10, cx),
4185 &["v root", " > target_destination/a/b/c/d"],
4186 "Moving first directory 'a' should move whole 'a/b/c/d' chain"
4187 );
4188}
4189
4190#[gpui::test]
4191async fn test_drag_entries_between_different_worktrees(cx: &mut gpui::TestAppContext) {
4192 init_test(cx);
4193
4194 let fs = FakeFs::new(cx.executor());
4195 fs.insert_tree(
4196 "/root_a",
4197 json!({
4198 "src": {
4199 "lib.rs": "",
4200 "main.rs": ""
4201 },
4202 "docs": {
4203 "guide.md": ""
4204 },
4205 "multi": {
4206 "alpha.txt": "",
4207 "beta.txt": ""
4208 }
4209 }),
4210 )
4211 .await;
4212 fs.insert_tree(
4213 "/root_b",
4214 json!({
4215 "dst": {
4216 "existing.md": ""
4217 },
4218 "target.txt": ""
4219 }),
4220 )
4221 .await;
4222
4223 let project = Project::test(fs.clone(), ["/root_a".as_ref(), "/root_b".as_ref()], cx).await;
4224 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4225 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4226 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4227 cx.run_until_parked();
4228
4229 // Case 1: move a file onto a directory in another worktree.
4230 select_path(&panel, "root_a/src/main.rs", cx);
4231 drag_selection_to(&panel, "root_b/dst", false, cx);
4232 assert!(
4233 find_project_entry(&panel, "root_b/dst/main.rs", cx).is_some(),
4234 "Dragged file should appear under destination worktree"
4235 );
4236 assert_eq!(
4237 find_project_entry(&panel, "root_a/src/main.rs", cx),
4238 None,
4239 "Dragged file should be removed from the source worktree"
4240 );
4241
4242 // Case 2: drop a file onto another worktree file so it lands in the parent directory.
4243 select_path(&panel, "root_a/docs/guide.md", cx);
4244 drag_selection_to(&panel, "root_b/dst/existing.md", true, cx);
4245 assert!(
4246 find_project_entry(&panel, "root_b/dst/guide.md", cx).is_some(),
4247 "Dropping onto a file should place the entry beside the target file"
4248 );
4249 assert_eq!(
4250 find_project_entry(&panel, "root_a/docs/guide.md", cx),
4251 None,
4252 "Source file should be removed after the move"
4253 );
4254
4255 // Case 3: move an entire directory.
4256 select_path(&panel, "root_a/src", cx);
4257 drag_selection_to(&panel, "root_b/dst", false, cx);
4258 assert!(
4259 find_project_entry(&panel, "root_b/dst/src/lib.rs", cx).is_some(),
4260 "Dragging a directory should move its nested contents"
4261 );
4262 assert_eq!(
4263 find_project_entry(&panel, "root_a/src", cx),
4264 None,
4265 "Directory should no longer exist in the source worktree"
4266 );
4267
4268 // Case 4: multi-selection drag between worktrees.
4269 panel.update(cx, |panel, _| panel.marked_entries.clear());
4270 select_path_with_mark(&panel, "root_a/multi/alpha.txt", cx);
4271 select_path_with_mark(&panel, "root_a/multi/beta.txt", cx);
4272 drag_selection_to(&panel, "root_b/dst", false, cx);
4273 assert!(
4274 find_project_entry(&panel, "root_b/dst/alpha.txt", cx).is_some()
4275 && find_project_entry(&panel, "root_b/dst/beta.txt", cx).is_some(),
4276 "All marked entries should move to the destination worktree"
4277 );
4278 assert_eq!(
4279 find_project_entry(&panel, "root_a/multi/alpha.txt", cx),
4280 None,
4281 "Marked entries should be removed from the origin worktree"
4282 );
4283 assert_eq!(
4284 find_project_entry(&panel, "root_a/multi/beta.txt", cx),
4285 None,
4286 "Marked entries should be removed from the origin worktree"
4287 );
4288}
4289
4290#[gpui::test]
4291async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
4292 init_test_with_editor(cx);
4293 cx.update(|cx| {
4294 cx.update_global::<SettingsStore, _>(|store, cx| {
4295 store.update_user_settings(cx, |settings| {
4296 settings.project.worktree.file_scan_exclusions = Some(Vec::new());
4297 settings
4298 .project_panel
4299 .get_or_insert_default()
4300 .auto_reveal_entries = Some(false);
4301 });
4302 })
4303 });
4304
4305 let fs = FakeFs::new(cx.background_executor.clone());
4306 fs.insert_tree(
4307 "/project_root",
4308 json!({
4309 ".git": {},
4310 ".gitignore": "**/gitignored_dir",
4311 "dir_1": {
4312 "file_1.py": "# File 1_1 contents",
4313 "file_2.py": "# File 1_2 contents",
4314 "file_3.py": "# File 1_3 contents",
4315 "gitignored_dir": {
4316 "file_a.py": "# File contents",
4317 "file_b.py": "# File contents",
4318 "file_c.py": "# File contents",
4319 },
4320 },
4321 "dir_2": {
4322 "file_1.py": "# File 2_1 contents",
4323 "file_2.py": "# File 2_2 contents",
4324 "file_3.py": "# File 2_3 contents",
4325 }
4326 }),
4327 )
4328 .await;
4329
4330 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
4331 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4332 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4333 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4334 cx.run_until_parked();
4335
4336 assert_eq!(
4337 visible_entries_as_strings(&panel, 0..20, cx),
4338 &[
4339 "v project_root",
4340 " > .git",
4341 " > dir_1",
4342 " > dir_2",
4343 " .gitignore",
4344 ]
4345 );
4346
4347 let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
4348 .expect("dir 1 file is not ignored and should have an entry");
4349 let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
4350 .expect("dir 2 file is not ignored and should have an entry");
4351 let gitignored_dir_file =
4352 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
4353 assert_eq!(
4354 gitignored_dir_file, None,
4355 "File in the gitignored dir should not have an entry before its dir is toggled"
4356 );
4357
4358 toggle_expand_dir(&panel, "project_root/dir_1", cx);
4359 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
4360 cx.executor().run_until_parked();
4361 assert_eq!(
4362 visible_entries_as_strings(&panel, 0..20, cx),
4363 &[
4364 "v project_root",
4365 " > .git",
4366 " v dir_1",
4367 " v gitignored_dir <== selected",
4368 " file_a.py",
4369 " file_b.py",
4370 " file_c.py",
4371 " file_1.py",
4372 " file_2.py",
4373 " file_3.py",
4374 " > dir_2",
4375 " .gitignore",
4376 ],
4377 "Should show gitignored dir file list in the project panel"
4378 );
4379 let gitignored_dir_file =
4380 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
4381 .expect("after gitignored dir got opened, a file entry should be present");
4382
4383 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
4384 toggle_expand_dir(&panel, "project_root/dir_1", cx);
4385 assert_eq!(
4386 visible_entries_as_strings(&panel, 0..20, cx),
4387 &[
4388 "v project_root",
4389 " > .git",
4390 " > dir_1 <== selected",
4391 " > dir_2",
4392 " .gitignore",
4393 ],
4394 "Should hide all dir contents again and prepare for the auto reveal test"
4395 );
4396
4397 for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
4398 panel.update(cx, |panel, cx| {
4399 panel.project.update(cx, |_, cx| {
4400 cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
4401 })
4402 });
4403 cx.run_until_parked();
4404 assert_eq!(
4405 visible_entries_as_strings(&panel, 0..20, cx),
4406 &[
4407 "v project_root",
4408 " > .git",
4409 " > dir_1 <== selected",
4410 " > dir_2",
4411 " .gitignore",
4412 ],
4413 "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
4414 );
4415 }
4416
4417 cx.update(|_, cx| {
4418 cx.update_global::<SettingsStore, _>(|store, cx| {
4419 store.update_user_settings(cx, |settings| {
4420 settings
4421 .project_panel
4422 .get_or_insert_default()
4423 .auto_reveal_entries = Some(true)
4424 });
4425 })
4426 });
4427
4428 panel.update(cx, |panel, cx| {
4429 panel.project.update(cx, |_, cx| {
4430 cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
4431 })
4432 });
4433 cx.run_until_parked();
4434 assert_eq!(
4435 visible_entries_as_strings(&panel, 0..20, cx),
4436 &[
4437 "v project_root",
4438 " > .git",
4439 " v dir_1",
4440 " > gitignored_dir",
4441 " file_1.py <== selected <== marked",
4442 " file_2.py",
4443 " file_3.py",
4444 " > dir_2",
4445 " .gitignore",
4446 ],
4447 "When auto reveal is enabled, not ignored dir_1 entry should be revealed"
4448 );
4449
4450 panel.update(cx, |panel, cx| {
4451 panel.project.update(cx, |_, cx| {
4452 cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
4453 })
4454 });
4455 cx.run_until_parked();
4456 assert_eq!(
4457 visible_entries_as_strings(&panel, 0..20, cx),
4458 &[
4459 "v project_root",
4460 " > .git",
4461 " v dir_1",
4462 " > gitignored_dir",
4463 " file_1.py",
4464 " file_2.py",
4465 " file_3.py",
4466 " v dir_2",
4467 " file_1.py <== selected <== marked",
4468 " file_2.py",
4469 " file_3.py",
4470 " .gitignore",
4471 ],
4472 "When auto reveal is enabled, not ignored dir_2 entry should be revealed"
4473 );
4474
4475 panel.update(cx, |panel, cx| {
4476 panel.project.update(cx, |_, cx| {
4477 cx.emit(project::Event::ActiveEntryChanged(Some(
4478 gitignored_dir_file,
4479 )))
4480 })
4481 });
4482 cx.run_until_parked();
4483 assert_eq!(
4484 visible_entries_as_strings(&panel, 0..20, cx),
4485 &[
4486 "v project_root",
4487 " > .git",
4488 " v dir_1",
4489 " > gitignored_dir",
4490 " file_1.py",
4491 " file_2.py",
4492 " file_3.py",
4493 " v dir_2",
4494 " file_1.py <== selected <== marked",
4495 " file_2.py",
4496 " file_3.py",
4497 " .gitignore",
4498 ],
4499 "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
4500 );
4501
4502 panel.update(cx, |panel, cx| {
4503 panel.project.update(cx, |_, cx| {
4504 cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
4505 })
4506 });
4507 cx.run_until_parked();
4508 assert_eq!(
4509 visible_entries_as_strings(&panel, 0..20, cx),
4510 &[
4511 "v project_root",
4512 " > .git",
4513 " v dir_1",
4514 " v gitignored_dir",
4515 " file_a.py <== selected <== marked",
4516 " file_b.py",
4517 " file_c.py",
4518 " file_1.py",
4519 " file_2.py",
4520 " file_3.py",
4521 " v dir_2",
4522 " file_1.py",
4523 " file_2.py",
4524 " file_3.py",
4525 " .gitignore",
4526 ],
4527 "When a gitignored entry is explicitly revealed, it should be shown in the project tree"
4528 );
4529}
4530
4531#[gpui::test]
4532async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
4533 init_test_with_editor(cx);
4534 cx.update(|cx| {
4535 cx.update_global::<SettingsStore, _>(|store, cx| {
4536 store.update_user_settings(cx, |settings| {
4537 settings.project.worktree.file_scan_exclusions = Some(Vec::new());
4538 settings.project.worktree.file_scan_inclusions =
4539 Some(vec!["always_included_but_ignored_dir/*".to_string()]);
4540 settings
4541 .project_panel
4542 .get_or_insert_default()
4543 .auto_reveal_entries = Some(false)
4544 });
4545 })
4546 });
4547
4548 let fs = FakeFs::new(cx.background_executor.clone());
4549 fs.insert_tree(
4550 "/project_root",
4551 json!({
4552 ".git": {},
4553 ".gitignore": "**/gitignored_dir\n/always_included_but_ignored_dir",
4554 "dir_1": {
4555 "file_1.py": "# File 1_1 contents",
4556 "file_2.py": "# File 1_2 contents",
4557 "file_3.py": "# File 1_3 contents",
4558 "gitignored_dir": {
4559 "file_a.py": "# File contents",
4560 "file_b.py": "# File contents",
4561 "file_c.py": "# File contents",
4562 },
4563 },
4564 "dir_2": {
4565 "file_1.py": "# File 2_1 contents",
4566 "file_2.py": "# File 2_2 contents",
4567 "file_3.py": "# File 2_3 contents",
4568 },
4569 "always_included_but_ignored_dir": {
4570 "file_a.py": "# File contents",
4571 "file_b.py": "# File contents",
4572 "file_c.py": "# File contents",
4573 },
4574 }),
4575 )
4576 .await;
4577
4578 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
4579 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4580 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4581 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4582 cx.run_until_parked();
4583
4584 assert_eq!(
4585 visible_entries_as_strings(&panel, 0..20, cx),
4586 &[
4587 "v project_root",
4588 " > .git",
4589 " > always_included_but_ignored_dir",
4590 " > dir_1",
4591 " > dir_2",
4592 " .gitignore",
4593 ]
4594 );
4595
4596 let gitignored_dir_file =
4597 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
4598 let always_included_but_ignored_dir_file = find_project_entry(
4599 &panel,
4600 "project_root/always_included_but_ignored_dir/file_a.py",
4601 cx,
4602 )
4603 .expect("file that is .gitignored but set to always be included should have an entry");
4604 assert_eq!(
4605 gitignored_dir_file, None,
4606 "File in the gitignored dir should not have an entry unless its directory is toggled"
4607 );
4608
4609 toggle_expand_dir(&panel, "project_root/dir_1", cx);
4610 cx.run_until_parked();
4611 cx.update(|_, cx| {
4612 cx.update_global::<SettingsStore, _>(|store, cx| {
4613 store.update_user_settings(cx, |settings| {
4614 settings
4615 .project_panel
4616 .get_or_insert_default()
4617 .auto_reveal_entries = Some(true)
4618 });
4619 })
4620 });
4621
4622 panel.update(cx, |panel, cx| {
4623 panel.project.update(cx, |_, cx| {
4624 cx.emit(project::Event::ActiveEntryChanged(Some(
4625 always_included_but_ignored_dir_file,
4626 )))
4627 })
4628 });
4629 cx.run_until_parked();
4630
4631 assert_eq!(
4632 visible_entries_as_strings(&panel, 0..20, cx),
4633 &[
4634 "v project_root",
4635 " > .git",
4636 " v always_included_but_ignored_dir",
4637 " file_a.py <== selected <== marked",
4638 " file_b.py",
4639 " file_c.py",
4640 " v dir_1",
4641 " > gitignored_dir",
4642 " file_1.py",
4643 " file_2.py",
4644 " file_3.py",
4645 " > dir_2",
4646 " .gitignore",
4647 ],
4648 "When auto reveal is enabled, a gitignored but always included selected entry should be revealed in the project panel"
4649 );
4650}
4651
4652#[gpui::test]
4653async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
4654 init_test_with_editor(cx);
4655 cx.update(|cx| {
4656 cx.update_global::<SettingsStore, _>(|store, cx| {
4657 store.update_user_settings(cx, |settings| {
4658 settings.project.worktree.file_scan_exclusions = Some(Vec::new());
4659 settings
4660 .project_panel
4661 .get_or_insert_default()
4662 .auto_reveal_entries = Some(false)
4663 });
4664 })
4665 });
4666
4667 let fs = FakeFs::new(cx.background_executor.clone());
4668 fs.insert_tree(
4669 "/project_root",
4670 json!({
4671 ".git": {},
4672 ".gitignore": "**/gitignored_dir",
4673 "dir_1": {
4674 "file_1.py": "# File 1_1 contents",
4675 "file_2.py": "# File 1_2 contents",
4676 "file_3.py": "# File 1_3 contents",
4677 "gitignored_dir": {
4678 "file_a.py": "# File contents",
4679 "file_b.py": "# File contents",
4680 "file_c.py": "# File contents",
4681 },
4682 },
4683 "dir_2": {
4684 "file_1.py": "# File 2_1 contents",
4685 "file_2.py": "# File 2_2 contents",
4686 "file_3.py": "# File 2_3 contents",
4687 }
4688 }),
4689 )
4690 .await;
4691
4692 let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
4693 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4694 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4695 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4696 cx.run_until_parked();
4697
4698 assert_eq!(
4699 visible_entries_as_strings(&panel, 0..20, cx),
4700 &[
4701 "v project_root",
4702 " > .git",
4703 " > dir_1",
4704 " > dir_2",
4705 " .gitignore",
4706 ]
4707 );
4708
4709 let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
4710 .expect("dir 1 file is not ignored and should have an entry");
4711 let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
4712 .expect("dir 2 file is not ignored and should have an entry");
4713 let gitignored_dir_file =
4714 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
4715 assert_eq!(
4716 gitignored_dir_file, None,
4717 "File in the gitignored dir should not have an entry before its dir is toggled"
4718 );
4719
4720 toggle_expand_dir(&panel, "project_root/dir_1", cx);
4721 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
4722 cx.run_until_parked();
4723 assert_eq!(
4724 visible_entries_as_strings(&panel, 0..20, cx),
4725 &[
4726 "v project_root",
4727 " > .git",
4728 " v dir_1",
4729 " v gitignored_dir <== selected",
4730 " file_a.py",
4731 " file_b.py",
4732 " file_c.py",
4733 " file_1.py",
4734 " file_2.py",
4735 " file_3.py",
4736 " > dir_2",
4737 " .gitignore",
4738 ],
4739 "Should show gitignored dir file list in the project panel"
4740 );
4741 let gitignored_dir_file =
4742 find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
4743 .expect("after gitignored dir got opened, a file entry should be present");
4744
4745 toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
4746 toggle_expand_dir(&panel, "project_root/dir_1", cx);
4747 assert_eq!(
4748 visible_entries_as_strings(&panel, 0..20, cx),
4749 &[
4750 "v project_root",
4751 " > .git",
4752 " > dir_1 <== selected",
4753 " > dir_2",
4754 " .gitignore",
4755 ],
4756 "Should hide all dir contents again and prepare for the explicit reveal test"
4757 );
4758
4759 for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
4760 panel.update(cx, |panel, cx| {
4761 panel.project.update(cx, |_, cx| {
4762 cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
4763 })
4764 });
4765 cx.run_until_parked();
4766 assert_eq!(
4767 visible_entries_as_strings(&panel, 0..20, cx),
4768 &[
4769 "v project_root",
4770 " > .git",
4771 " > dir_1 <== selected",
4772 " > dir_2",
4773 " .gitignore",
4774 ],
4775 "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
4776 );
4777 }
4778
4779 panel.update(cx, |panel, cx| {
4780 panel.project.update(cx, |_, cx| {
4781 cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
4782 })
4783 });
4784 cx.run_until_parked();
4785 assert_eq!(
4786 visible_entries_as_strings(&panel, 0..20, cx),
4787 &[
4788 "v project_root",
4789 " > .git",
4790 " v dir_1",
4791 " > gitignored_dir",
4792 " file_1.py <== selected <== marked",
4793 " file_2.py",
4794 " file_3.py",
4795 " > dir_2",
4796 " .gitignore",
4797 ],
4798 "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
4799 );
4800
4801 panel.update(cx, |panel, cx| {
4802 panel.project.update(cx, |_, cx| {
4803 cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
4804 })
4805 });
4806 cx.run_until_parked();
4807 assert_eq!(
4808 visible_entries_as_strings(&panel, 0..20, cx),
4809 &[
4810 "v project_root",
4811 " > .git",
4812 " v dir_1",
4813 " > gitignored_dir",
4814 " file_1.py",
4815 " file_2.py",
4816 " file_3.py",
4817 " v dir_2",
4818 " file_1.py <== selected <== marked",
4819 " file_2.py",
4820 " file_3.py",
4821 " .gitignore",
4822 ],
4823 "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
4824 );
4825
4826 panel.update(cx, |panel, cx| {
4827 panel.project.update(cx, |_, cx| {
4828 cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
4829 })
4830 });
4831 cx.run_until_parked();
4832 assert_eq!(
4833 visible_entries_as_strings(&panel, 0..20, cx),
4834 &[
4835 "v project_root",
4836 " > .git",
4837 " v dir_1",
4838 " v gitignored_dir",
4839 " file_a.py <== selected <== marked",
4840 " file_b.py",
4841 " file_c.py",
4842 " file_1.py",
4843 " file_2.py",
4844 " file_3.py",
4845 " v dir_2",
4846 " file_1.py",
4847 " file_2.py",
4848 " file_3.py",
4849 " .gitignore",
4850 ],
4851 "With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
4852 );
4853}
4854
4855#[gpui::test]
4856async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
4857 init_test(cx);
4858 cx.update(|cx| {
4859 cx.update_global::<SettingsStore, _>(|store, cx| {
4860 store.update_user_settings(cx, |settings| {
4861 settings.project.worktree.file_scan_exclusions =
4862 Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
4863 });
4864 });
4865 });
4866
4867 cx.update(|cx| {
4868 register_project_item::<TestProjectItemView>(cx);
4869 });
4870
4871 let fs = FakeFs::new(cx.executor());
4872 fs.insert_tree(
4873 "/root1",
4874 json!({
4875 ".dockerignore": "",
4876 ".git": {
4877 "HEAD": "",
4878 },
4879 }),
4880 )
4881 .await;
4882
4883 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
4884 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4885 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4886 let panel = workspace
4887 .update(cx, |workspace, window, cx| {
4888 let panel = ProjectPanel::new(workspace, window, cx);
4889 workspace.add_panel(panel.clone(), window, cx);
4890 panel
4891 })
4892 .unwrap();
4893 cx.run_until_parked();
4894
4895 select_path(&panel, "root1", cx);
4896 assert_eq!(
4897 visible_entries_as_strings(&panel, 0..10, cx),
4898 &["v root1 <== selected", " .dockerignore",]
4899 );
4900 workspace
4901 .update(cx, |workspace, _, cx| {
4902 assert!(
4903 workspace.active_item(cx).is_none(),
4904 "Should have no active items in the beginning"
4905 );
4906 })
4907 .unwrap();
4908
4909 let excluded_file_path = ".git/COMMIT_EDITMSG";
4910 let excluded_dir_path = "excluded_dir";
4911
4912 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
4913 cx.run_until_parked();
4914 panel.update_in(cx, |panel, window, cx| {
4915 assert!(panel.filename_editor.read(cx).is_focused(window));
4916 });
4917 panel
4918 .update_in(cx, |panel, window, cx| {
4919 panel.filename_editor.update(cx, |editor, cx| {
4920 editor.set_text(excluded_file_path, window, cx)
4921 });
4922 panel.confirm_edit(true, window, cx).unwrap()
4923 })
4924 .await
4925 .unwrap();
4926
4927 assert_eq!(
4928 visible_entries_as_strings(&panel, 0..13, cx),
4929 &["v root1", " .dockerignore"],
4930 "Excluded dir should not be shown after opening a file in it"
4931 );
4932 panel.update_in(cx, |panel, window, cx| {
4933 assert!(
4934 !panel.filename_editor.read(cx).is_focused(window),
4935 "Should have closed the file name editor"
4936 );
4937 });
4938 workspace
4939 .update(cx, |workspace, _, cx| {
4940 let active_entry_path = workspace
4941 .active_item(cx)
4942 .expect("should have opened and activated the excluded item")
4943 .act_as::<TestProjectItemView>(cx)
4944 .expect("should have opened the corresponding project item for the excluded item")
4945 .read(cx)
4946 .path
4947 .clone();
4948 assert_eq!(
4949 active_entry_path.path.as_ref(),
4950 rel_path(excluded_file_path),
4951 "Should open the excluded file"
4952 );
4953
4954 assert!(
4955 workspace.notification_ids().is_empty(),
4956 "Should have no notifications after opening an excluded file"
4957 );
4958 })
4959 .unwrap();
4960 assert!(
4961 fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
4962 "Should have created the excluded file"
4963 );
4964
4965 select_path(&panel, "root1", cx);
4966 panel.update_in(cx, |panel, window, cx| {
4967 panel.new_directory(&NewDirectory, window, cx)
4968 });
4969 cx.run_until_parked();
4970 panel.update_in(cx, |panel, window, cx| {
4971 assert!(panel.filename_editor.read(cx).is_focused(window));
4972 });
4973 panel
4974 .update_in(cx, |panel, window, cx| {
4975 panel.filename_editor.update(cx, |editor, cx| {
4976 editor.set_text(excluded_file_path, window, cx)
4977 });
4978 panel.confirm_edit(true, window, cx).unwrap()
4979 })
4980 .await
4981 .unwrap();
4982 cx.run_until_parked();
4983 assert_eq!(
4984 visible_entries_as_strings(&panel, 0..13, cx),
4985 &["v root1", " .dockerignore"],
4986 "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
4987 );
4988 panel.update_in(cx, |panel, window, cx| {
4989 assert!(
4990 !panel.filename_editor.read(cx).is_focused(window),
4991 "Should have closed the file name editor"
4992 );
4993 });
4994 workspace
4995 .update(cx, |workspace, _, cx| {
4996 let notifications = workspace.notification_ids();
4997 assert_eq!(
4998 notifications.len(),
4999 1,
5000 "Should receive one notification with the error message"
5001 );
5002 workspace.dismiss_notification(notifications.first().unwrap(), cx);
5003 assert!(workspace.notification_ids().is_empty());
5004 })
5005 .unwrap();
5006
5007 select_path(&panel, "root1", cx);
5008 panel.update_in(cx, |panel, window, cx| {
5009 panel.new_directory(&NewDirectory, window, cx)
5010 });
5011 cx.run_until_parked();
5012
5013 panel.update_in(cx, |panel, window, cx| {
5014 assert!(panel.filename_editor.read(cx).is_focused(window));
5015 });
5016
5017 panel
5018 .update_in(cx, |panel, window, cx| {
5019 panel.filename_editor.update(cx, |editor, cx| {
5020 editor.set_text(excluded_dir_path, window, cx)
5021 });
5022 panel.confirm_edit(true, window, cx).unwrap()
5023 })
5024 .await
5025 .unwrap();
5026
5027 cx.run_until_parked();
5028
5029 assert_eq!(
5030 visible_entries_as_strings(&panel, 0..13, cx),
5031 &["v root1", " .dockerignore"],
5032 "Should not change the project panel after trying to create an excluded directory"
5033 );
5034 panel.update_in(cx, |panel, window, cx| {
5035 assert!(
5036 !panel.filename_editor.read(cx).is_focused(window),
5037 "Should have closed the file name editor"
5038 );
5039 });
5040 workspace
5041 .update(cx, |workspace, _, cx| {
5042 let notifications = workspace.notification_ids();
5043 assert_eq!(
5044 notifications.len(),
5045 1,
5046 "Should receive one notification explaining that no directory is actually shown"
5047 );
5048 workspace.dismiss_notification(notifications.first().unwrap(), cx);
5049 assert!(workspace.notification_ids().is_empty());
5050 })
5051 .unwrap();
5052 assert!(
5053 fs.is_dir(Path::new("/root1/excluded_dir")).await,
5054 "Should have created the excluded directory"
5055 );
5056}
5057
5058#[gpui::test]
5059async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) {
5060 init_test_with_editor(cx);
5061
5062 let fs = FakeFs::new(cx.executor());
5063 fs.insert_tree(
5064 "/src",
5065 json!({
5066 "test": {
5067 "first.rs": "// First Rust file",
5068 "second.rs": "// Second Rust file",
5069 "third.rs": "// Third Rust file",
5070 }
5071 }),
5072 )
5073 .await;
5074
5075 let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
5076 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5077 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5078 let panel = workspace
5079 .update(cx, |workspace, window, cx| {
5080 let panel = ProjectPanel::new(workspace, window, cx);
5081 workspace.add_panel(panel.clone(), window, cx);
5082 panel
5083 })
5084 .unwrap();
5085 cx.run_until_parked();
5086
5087 select_path(&panel, "src", cx);
5088 panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
5089 cx.executor().run_until_parked();
5090 assert_eq!(
5091 visible_entries_as_strings(&panel, 0..10, cx),
5092 &[
5093 //
5094 "v src <== selected",
5095 " > test"
5096 ]
5097 );
5098 panel.update_in(cx, |panel, window, cx| {
5099 panel.new_directory(&NewDirectory, window, cx)
5100 });
5101 cx.executor().run_until_parked();
5102 panel.update_in(cx, |panel, window, cx| {
5103 assert!(panel.filename_editor.read(cx).is_focused(window));
5104 });
5105 assert_eq!(
5106 visible_entries_as_strings(&panel, 0..10, cx),
5107 &[
5108 //
5109 "v src",
5110 " > [EDITOR: ''] <== selected",
5111 " > test"
5112 ]
5113 );
5114
5115 panel.update_in(cx, |panel, window, cx| {
5116 panel.cancel(&menu::Cancel, window, cx);
5117 panel.update_visible_entries(None, false, false, window, cx);
5118 });
5119 cx.executor().run_until_parked();
5120 assert_eq!(
5121 visible_entries_as_strings(&panel, 0..10, cx),
5122 &[
5123 //
5124 "v src <== selected",
5125 " > test"
5126 ]
5127 );
5128}
5129
5130#[gpui::test]
5131async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
5132 init_test_with_editor(cx);
5133
5134 let fs = FakeFs::new(cx.executor());
5135 fs.insert_tree(
5136 "/root",
5137 json!({
5138 "dir1": {
5139 "subdir1": {},
5140 "file1.txt": "",
5141 "file2.txt": "",
5142 },
5143 "dir2": {
5144 "subdir2": {},
5145 "file3.txt": "",
5146 "file4.txt": "",
5147 },
5148 "file5.txt": "",
5149 "file6.txt": "",
5150 }),
5151 )
5152 .await;
5153
5154 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5155 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5156 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5157 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5158 cx.run_until_parked();
5159
5160 toggle_expand_dir(&panel, "root/dir1", cx);
5161 toggle_expand_dir(&panel, "root/dir2", cx);
5162
5163 // Test Case 1: Delete middle file in directory
5164 select_path(&panel, "root/dir1/file1.txt", cx);
5165 assert_eq!(
5166 visible_entries_as_strings(&panel, 0..15, cx),
5167 &[
5168 "v root",
5169 " v dir1",
5170 " > subdir1",
5171 " file1.txt <== selected",
5172 " file2.txt",
5173 " v dir2",
5174 " > subdir2",
5175 " file3.txt",
5176 " file4.txt",
5177 " file5.txt",
5178 " file6.txt",
5179 ],
5180 "Initial state before deleting middle file"
5181 );
5182
5183 submit_deletion(&panel, cx);
5184 assert_eq!(
5185 visible_entries_as_strings(&panel, 0..15, cx),
5186 &[
5187 "v root",
5188 " v dir1",
5189 " > subdir1",
5190 " file2.txt <== selected",
5191 " v dir2",
5192 " > subdir2",
5193 " file3.txt",
5194 " file4.txt",
5195 " file5.txt",
5196 " file6.txt",
5197 ],
5198 "Should select next file after deleting middle file"
5199 );
5200
5201 // Test Case 2: Delete last file in directory
5202 submit_deletion(&panel, cx);
5203 assert_eq!(
5204 visible_entries_as_strings(&panel, 0..15, cx),
5205 &[
5206 "v root",
5207 " v dir1",
5208 " > subdir1 <== selected",
5209 " v dir2",
5210 " > subdir2",
5211 " file3.txt",
5212 " file4.txt",
5213 " file5.txt",
5214 " file6.txt",
5215 ],
5216 "Should select next directory when last file is deleted"
5217 );
5218
5219 // Test Case 3: Delete root level file
5220 select_path(&panel, "root/file6.txt", cx);
5221 assert_eq!(
5222 visible_entries_as_strings(&panel, 0..15, cx),
5223 &[
5224 "v root",
5225 " v dir1",
5226 " > subdir1",
5227 " v dir2",
5228 " > subdir2",
5229 " file3.txt",
5230 " file4.txt",
5231 " file5.txt",
5232 " file6.txt <== selected",
5233 ],
5234 "Initial state before deleting root level file"
5235 );
5236
5237 submit_deletion(&panel, cx);
5238 assert_eq!(
5239 visible_entries_as_strings(&panel, 0..15, cx),
5240 &[
5241 "v root",
5242 " v dir1",
5243 " > subdir1",
5244 " v dir2",
5245 " > subdir2",
5246 " file3.txt",
5247 " file4.txt",
5248 " file5.txt <== selected",
5249 ],
5250 "Should select prev entry at root level"
5251 );
5252}
5253
5254#[gpui::test]
5255async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) {
5256 init_test_with_editor(cx);
5257
5258 let fs = FakeFs::new(cx.executor());
5259 fs.insert_tree(
5260 path!("/root"),
5261 json!({
5262 "aa": "// Testing 1",
5263 "bb": "// Testing 2",
5264 "cc": "// Testing 3",
5265 "dd": "// Testing 4",
5266 "ee": "// Testing 5",
5267 "ff": "// Testing 6",
5268 "gg": "// Testing 7",
5269 "hh": "// Testing 8",
5270 "ii": "// Testing 8",
5271 ".gitignore": "bb\ndd\nee\nff\nii\n'",
5272 }),
5273 )
5274 .await;
5275
5276 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
5277 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5278 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5279
5280 // Test 1: Auto selection with one gitignored file next to the deleted file
5281 cx.update(|_, cx| {
5282 let settings = *ProjectPanelSettings::get_global(cx);
5283 ProjectPanelSettings::override_global(
5284 ProjectPanelSettings {
5285 hide_gitignore: true,
5286 ..settings
5287 },
5288 cx,
5289 );
5290 });
5291
5292 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5293 cx.run_until_parked();
5294
5295 select_path(&panel, "root/aa", cx);
5296 assert_eq!(
5297 visible_entries_as_strings(&panel, 0..10, cx),
5298 &[
5299 "v root",
5300 " .gitignore",
5301 " aa <== selected",
5302 " cc",
5303 " gg",
5304 " hh"
5305 ],
5306 "Initial state should hide files on .gitignore"
5307 );
5308
5309 submit_deletion(&panel, cx);
5310
5311 assert_eq!(
5312 visible_entries_as_strings(&panel, 0..10, cx),
5313 &[
5314 "v root",
5315 " .gitignore",
5316 " cc <== selected",
5317 " gg",
5318 " hh"
5319 ],
5320 "Should select next entry not on .gitignore"
5321 );
5322
5323 // Test 2: Auto selection with many gitignored files next to the deleted file
5324 submit_deletion(&panel, cx);
5325 assert_eq!(
5326 visible_entries_as_strings(&panel, 0..10, cx),
5327 &[
5328 "v root",
5329 " .gitignore",
5330 " gg <== selected",
5331 " hh"
5332 ],
5333 "Should select next entry not on .gitignore"
5334 );
5335
5336 // Test 3: Auto selection of entry before deleted file
5337 select_path(&panel, "root/hh", cx);
5338 assert_eq!(
5339 visible_entries_as_strings(&panel, 0..10, cx),
5340 &[
5341 "v root",
5342 " .gitignore",
5343 " gg",
5344 " hh <== selected"
5345 ],
5346 "Should select next entry not on .gitignore"
5347 );
5348 submit_deletion(&panel, cx);
5349 assert_eq!(
5350 visible_entries_as_strings(&panel, 0..10, cx),
5351 &["v root", " .gitignore", " gg <== selected"],
5352 "Should select next entry not on .gitignore"
5353 );
5354}
5355
5356#[gpui::test]
5357async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) {
5358 init_test_with_editor(cx);
5359
5360 let fs = FakeFs::new(cx.executor());
5361 fs.insert_tree(
5362 path!("/root"),
5363 json!({
5364 "dir1": {
5365 "file1": "// Testing",
5366 "file2": "// Testing",
5367 "file3": "// Testing"
5368 },
5369 "aa": "// Testing",
5370 ".gitignore": "file1\nfile3\n",
5371 }),
5372 )
5373 .await;
5374
5375 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
5376 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5377 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5378
5379 cx.update(|_, cx| {
5380 let settings = *ProjectPanelSettings::get_global(cx);
5381 ProjectPanelSettings::override_global(
5382 ProjectPanelSettings {
5383 hide_gitignore: true,
5384 ..settings
5385 },
5386 cx,
5387 );
5388 });
5389
5390 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5391 cx.run_until_parked();
5392
5393 // Test 1: Visible items should exclude files on gitignore
5394 toggle_expand_dir(&panel, "root/dir1", cx);
5395 select_path(&panel, "root/dir1/file2", cx);
5396 assert_eq!(
5397 visible_entries_as_strings(&panel, 0..10, cx),
5398 &[
5399 "v root",
5400 " v dir1",
5401 " file2 <== selected",
5402 " .gitignore",
5403 " aa"
5404 ],
5405 "Initial state should hide files on .gitignore"
5406 );
5407 submit_deletion(&panel, cx);
5408
5409 // Test 2: Auto selection should go to the parent
5410 assert_eq!(
5411 visible_entries_as_strings(&panel, 0..10, cx),
5412 &[
5413 "v root",
5414 " v dir1 <== selected",
5415 " .gitignore",
5416 " aa"
5417 ],
5418 "Initial state should hide files on .gitignore"
5419 );
5420}
5421
5422#[gpui::test]
5423async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
5424 init_test_with_editor(cx);
5425
5426 let fs = FakeFs::new(cx.executor());
5427 fs.insert_tree(
5428 "/root",
5429 json!({
5430 "dir1": {
5431 "subdir1": {
5432 "a.txt": "",
5433 "b.txt": ""
5434 },
5435 "file1.txt": "",
5436 },
5437 "dir2": {
5438 "subdir2": {
5439 "c.txt": "",
5440 "d.txt": ""
5441 },
5442 "file2.txt": "",
5443 },
5444 "file3.txt": "",
5445 }),
5446 )
5447 .await;
5448
5449 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5450 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5451 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5452 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5453 cx.run_until_parked();
5454
5455 toggle_expand_dir(&panel, "root/dir1", cx);
5456 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
5457 toggle_expand_dir(&panel, "root/dir2", cx);
5458 toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
5459
5460 // Test Case 1: Select and delete nested directory with parent
5461 cx.simulate_modifiers_change(gpui::Modifiers {
5462 control: true,
5463 ..Default::default()
5464 });
5465 select_path_with_mark(&panel, "root/dir1/subdir1", cx);
5466 select_path_with_mark(&panel, "root/dir1", cx);
5467
5468 assert_eq!(
5469 visible_entries_as_strings(&panel, 0..15, cx),
5470 &[
5471 "v root",
5472 " v dir1 <== selected <== marked",
5473 " v subdir1 <== marked",
5474 " a.txt",
5475 " b.txt",
5476 " file1.txt",
5477 " v dir2",
5478 " v subdir2",
5479 " c.txt",
5480 " d.txt",
5481 " file2.txt",
5482 " file3.txt",
5483 ],
5484 "Initial state before deleting nested directory with parent"
5485 );
5486
5487 submit_deletion(&panel, cx);
5488 assert_eq!(
5489 visible_entries_as_strings(&panel, 0..15, cx),
5490 &[
5491 "v root",
5492 " v dir2 <== selected",
5493 " v subdir2",
5494 " c.txt",
5495 " d.txt",
5496 " file2.txt",
5497 " file3.txt",
5498 ],
5499 "Should select next directory after deleting directory with parent"
5500 );
5501
5502 // Test Case 2: Select mixed files and directories across levels
5503 select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx);
5504 select_path_with_mark(&panel, "root/dir2/file2.txt", cx);
5505 select_path_with_mark(&panel, "root/file3.txt", cx);
5506
5507 assert_eq!(
5508 visible_entries_as_strings(&panel, 0..15, cx),
5509 &[
5510 "v root",
5511 " v dir2",
5512 " v subdir2",
5513 " c.txt <== marked",
5514 " d.txt",
5515 " file2.txt <== marked",
5516 " file3.txt <== selected <== marked",
5517 ],
5518 "Initial state before deleting"
5519 );
5520
5521 submit_deletion(&panel, cx);
5522 assert_eq!(
5523 visible_entries_as_strings(&panel, 0..15, cx),
5524 &[
5525 "v root",
5526 " v dir2 <== selected",
5527 " v subdir2",
5528 " d.txt",
5529 ],
5530 "Should select sibling directory"
5531 );
5532}
5533
5534#[gpui::test]
5535async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) {
5536 init_test_with_editor(cx);
5537
5538 let fs = FakeFs::new(cx.executor());
5539 fs.insert_tree(
5540 "/root",
5541 json!({
5542 "dir1": {
5543 "subdir1": {
5544 "a.txt": "",
5545 "b.txt": ""
5546 },
5547 "file1.txt": "",
5548 },
5549 "dir2": {
5550 "subdir2": {
5551 "c.txt": "",
5552 "d.txt": ""
5553 },
5554 "file2.txt": "",
5555 },
5556 "file3.txt": "",
5557 "file4.txt": "",
5558 }),
5559 )
5560 .await;
5561
5562 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5563 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5564 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5565 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5566 cx.run_until_parked();
5567
5568 toggle_expand_dir(&panel, "root/dir1", cx);
5569 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
5570 toggle_expand_dir(&panel, "root/dir2", cx);
5571 toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
5572
5573 // Test Case 1: Select all root files and directories
5574 cx.simulate_modifiers_change(gpui::Modifiers {
5575 control: true,
5576 ..Default::default()
5577 });
5578 select_path_with_mark(&panel, "root/dir1", cx);
5579 select_path_with_mark(&panel, "root/dir2", cx);
5580 select_path_with_mark(&panel, "root/file3.txt", cx);
5581 select_path_with_mark(&panel, "root/file4.txt", cx);
5582 assert_eq!(
5583 visible_entries_as_strings(&panel, 0..20, cx),
5584 &[
5585 "v root",
5586 " v dir1 <== marked",
5587 " v subdir1",
5588 " a.txt",
5589 " b.txt",
5590 " file1.txt",
5591 " v dir2 <== marked",
5592 " v subdir2",
5593 " c.txt",
5594 " d.txt",
5595 " file2.txt",
5596 " file3.txt <== marked",
5597 " file4.txt <== selected <== marked",
5598 ],
5599 "State before deleting all contents"
5600 );
5601
5602 submit_deletion(&panel, cx);
5603 assert_eq!(
5604 visible_entries_as_strings(&panel, 0..20, cx),
5605 &["v root <== selected"],
5606 "Only empty root directory should remain after deleting all contents"
5607 );
5608}
5609
5610#[gpui::test]
5611async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) {
5612 init_test_with_editor(cx);
5613
5614 let fs = FakeFs::new(cx.executor());
5615 fs.insert_tree(
5616 "/root",
5617 json!({
5618 "dir1": {
5619 "subdir1": {
5620 "file_a.txt": "content a",
5621 "file_b.txt": "content b",
5622 },
5623 "subdir2": {
5624 "file_c.txt": "content c",
5625 },
5626 "file1.txt": "content 1",
5627 },
5628 "dir2": {
5629 "file2.txt": "content 2",
5630 },
5631 }),
5632 )
5633 .await;
5634
5635 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5636 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5637 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5638 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5639 cx.run_until_parked();
5640
5641 toggle_expand_dir(&panel, "root/dir1", cx);
5642 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
5643 toggle_expand_dir(&panel, "root/dir2", cx);
5644 cx.simulate_modifiers_change(gpui::Modifiers {
5645 control: true,
5646 ..Default::default()
5647 });
5648
5649 // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory
5650 select_path_with_mark(&panel, "root/dir1", cx);
5651 select_path_with_mark(&panel, "root/dir1/subdir1", cx);
5652 select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx);
5653
5654 assert_eq!(
5655 visible_entries_as_strings(&panel, 0..20, cx),
5656 &[
5657 "v root",
5658 " v dir1 <== marked",
5659 " v subdir1 <== marked",
5660 " file_a.txt <== selected <== marked",
5661 " file_b.txt",
5662 " > subdir2",
5663 " file1.txt",
5664 " v dir2",
5665 " file2.txt",
5666 ],
5667 "State with parent dir, subdir, and file selected"
5668 );
5669 submit_deletion(&panel, cx);
5670 assert_eq!(
5671 visible_entries_as_strings(&panel, 0..20, cx),
5672 &["v root", " v dir2 <== selected", " file2.txt",],
5673 "Only dir2 should remain after deletion"
5674 );
5675}
5676
5677#[gpui::test]
5678async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) {
5679 init_test_with_editor(cx);
5680
5681 let fs = FakeFs::new(cx.executor());
5682 // First worktree
5683 fs.insert_tree(
5684 "/root1",
5685 json!({
5686 "dir1": {
5687 "file1.txt": "content 1",
5688 "file2.txt": "content 2",
5689 },
5690 "dir2": {
5691 "file3.txt": "content 3",
5692 },
5693 }),
5694 )
5695 .await;
5696
5697 // Second worktree
5698 fs.insert_tree(
5699 "/root2",
5700 json!({
5701 "dir3": {
5702 "file4.txt": "content 4",
5703 "file5.txt": "content 5",
5704 },
5705 "file6.txt": "content 6",
5706 }),
5707 )
5708 .await;
5709
5710 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
5711 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5712 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5713 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5714 cx.run_until_parked();
5715
5716 // Expand all directories for testing
5717 toggle_expand_dir(&panel, "root1/dir1", cx);
5718 toggle_expand_dir(&panel, "root1/dir2", cx);
5719 toggle_expand_dir(&panel, "root2/dir3", cx);
5720
5721 // Test Case 1: Delete files across different worktrees
5722 cx.simulate_modifiers_change(gpui::Modifiers {
5723 control: true,
5724 ..Default::default()
5725 });
5726 select_path_with_mark(&panel, "root1/dir1/file1.txt", cx);
5727 select_path_with_mark(&panel, "root2/dir3/file4.txt", cx);
5728
5729 assert_eq!(
5730 visible_entries_as_strings(&panel, 0..20, cx),
5731 &[
5732 "v root1",
5733 " v dir1",
5734 " file1.txt <== marked",
5735 " file2.txt",
5736 " v dir2",
5737 " file3.txt",
5738 "v root2",
5739 " v dir3",
5740 " file4.txt <== selected <== marked",
5741 " file5.txt",
5742 " file6.txt",
5743 ],
5744 "Initial state with files selected from different worktrees"
5745 );
5746
5747 submit_deletion(&panel, cx);
5748 assert_eq!(
5749 visible_entries_as_strings(&panel, 0..20, cx),
5750 &[
5751 "v root1",
5752 " v dir1",
5753 " file2.txt",
5754 " v dir2",
5755 " file3.txt",
5756 "v root2",
5757 " v dir3",
5758 " file5.txt <== selected",
5759 " file6.txt",
5760 ],
5761 "Should select next file in the last worktree after deletion"
5762 );
5763
5764 // Test Case 2: Delete directories from different worktrees
5765 select_path_with_mark(&panel, "root1/dir1", cx);
5766 select_path_with_mark(&panel, "root2/dir3", cx);
5767
5768 assert_eq!(
5769 visible_entries_as_strings(&panel, 0..20, cx),
5770 &[
5771 "v root1",
5772 " v dir1 <== marked",
5773 " file2.txt",
5774 " v dir2",
5775 " file3.txt",
5776 "v root2",
5777 " v dir3 <== selected <== marked",
5778 " file5.txt",
5779 " file6.txt",
5780 ],
5781 "State with directories marked from different worktrees"
5782 );
5783
5784 submit_deletion(&panel, cx);
5785 assert_eq!(
5786 visible_entries_as_strings(&panel, 0..20, cx),
5787 &[
5788 "v root1",
5789 " v dir2",
5790 " file3.txt",
5791 "v root2",
5792 " file6.txt <== selected",
5793 ],
5794 "Should select remaining file in last worktree after directory deletion"
5795 );
5796
5797 // Test Case 4: Delete all remaining files except roots
5798 select_path_with_mark(&panel, "root1/dir2/file3.txt", cx);
5799 select_path_with_mark(&panel, "root2/file6.txt", cx);
5800
5801 assert_eq!(
5802 visible_entries_as_strings(&panel, 0..20, cx),
5803 &[
5804 "v root1",
5805 " v dir2",
5806 " file3.txt <== marked",
5807 "v root2",
5808 " file6.txt <== selected <== marked",
5809 ],
5810 "State with all remaining files marked"
5811 );
5812
5813 submit_deletion(&panel, cx);
5814 assert_eq!(
5815 visible_entries_as_strings(&panel, 0..20, cx),
5816 &["v root1", " v dir2", "v root2 <== selected"],
5817 "Second parent root should be selected after deleting"
5818 );
5819}
5820
5821#[gpui::test]
5822async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) {
5823 init_test_with_editor(cx);
5824
5825 let fs = FakeFs::new(cx.executor());
5826 fs.insert_tree(
5827 "/root",
5828 json!({
5829 "dir1": {
5830 "file1.txt": "",
5831 "file2.txt": "",
5832 "file3.txt": "",
5833 },
5834 "dir2": {
5835 "file4.txt": "",
5836 "file5.txt": "",
5837 },
5838 }),
5839 )
5840 .await;
5841
5842 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
5843 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5844 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5845 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5846 cx.run_until_parked();
5847
5848 toggle_expand_dir(&panel, "root/dir1", cx);
5849 toggle_expand_dir(&panel, "root/dir2", cx);
5850
5851 cx.simulate_modifiers_change(gpui::Modifiers {
5852 control: true,
5853 ..Default::default()
5854 });
5855
5856 select_path_with_mark(&panel, "root/dir1/file2.txt", cx);
5857 select_path(&panel, "root/dir1/file1.txt", cx);
5858
5859 assert_eq!(
5860 visible_entries_as_strings(&panel, 0..15, cx),
5861 &[
5862 "v root",
5863 " v dir1",
5864 " file1.txt <== selected",
5865 " file2.txt <== marked",
5866 " file3.txt",
5867 " v dir2",
5868 " file4.txt",
5869 " file5.txt",
5870 ],
5871 "Initial state with one marked entry and different selection"
5872 );
5873
5874 // Delete should operate on the selected entry (file1.txt)
5875 submit_deletion(&panel, cx);
5876 assert_eq!(
5877 visible_entries_as_strings(&panel, 0..15, cx),
5878 &[
5879 "v root",
5880 " v dir1",
5881 " file2.txt <== selected <== marked",
5882 " file3.txt",
5883 " v dir2",
5884 " file4.txt",
5885 " file5.txt",
5886 ],
5887 "Should delete selected file, not marked file"
5888 );
5889
5890 select_path_with_mark(&panel, "root/dir1/file3.txt", cx);
5891 select_path_with_mark(&panel, "root/dir2/file4.txt", cx);
5892 select_path(&panel, "root/dir2/file5.txt", cx);
5893
5894 assert_eq!(
5895 visible_entries_as_strings(&panel, 0..15, cx),
5896 &[
5897 "v root",
5898 " v dir1",
5899 " file2.txt <== marked",
5900 " file3.txt <== marked",
5901 " v dir2",
5902 " file4.txt <== marked",
5903 " file5.txt <== selected",
5904 ],
5905 "Initial state with multiple marked entries and different selection"
5906 );
5907
5908 // Delete should operate on all marked entries, ignoring the selection
5909 submit_deletion(&panel, cx);
5910 assert_eq!(
5911 visible_entries_as_strings(&panel, 0..15, cx),
5912 &[
5913 "v root",
5914 " v dir1",
5915 " v dir2",
5916 " file5.txt <== selected",
5917 ],
5918 "Should delete all marked files, leaving only the selected file"
5919 );
5920}
5921
5922#[gpui::test]
5923async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) {
5924 init_test_with_editor(cx);
5925
5926 let fs = FakeFs::new(cx.executor());
5927 fs.insert_tree(
5928 "/root_b",
5929 json!({
5930 "dir1": {
5931 "file1.txt": "content 1",
5932 "file2.txt": "content 2",
5933 },
5934 }),
5935 )
5936 .await;
5937
5938 fs.insert_tree(
5939 "/root_c",
5940 json!({
5941 "dir2": {},
5942 }),
5943 )
5944 .await;
5945
5946 let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await;
5947 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5948 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5949 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5950 cx.run_until_parked();
5951
5952 toggle_expand_dir(&panel, "root_b/dir1", cx);
5953 toggle_expand_dir(&panel, "root_c/dir2", cx);
5954
5955 cx.simulate_modifiers_change(gpui::Modifiers {
5956 control: true,
5957 ..Default::default()
5958 });
5959 select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx);
5960 select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx);
5961
5962 assert_eq!(
5963 visible_entries_as_strings(&panel, 0..20, cx),
5964 &[
5965 "v root_b",
5966 " v dir1",
5967 " file1.txt <== marked",
5968 " file2.txt <== selected <== marked",
5969 "v root_c",
5970 " v dir2",
5971 ],
5972 "Initial state with files marked in root_b"
5973 );
5974
5975 submit_deletion(&panel, cx);
5976 assert_eq!(
5977 visible_entries_as_strings(&panel, 0..20, cx),
5978 &[
5979 "v root_b",
5980 " v dir1 <== selected",
5981 "v root_c",
5982 " v dir2",
5983 ],
5984 "After deletion in root_b as it's last deletion, selection should be in root_b"
5985 );
5986
5987 select_path_with_mark(&panel, "root_c/dir2", cx);
5988
5989 submit_deletion(&panel, cx);
5990 assert_eq!(
5991 visible_entries_as_strings(&panel, 0..20, cx),
5992 &["v root_b", " v dir1", "v root_c <== selected",],
5993 "After deleting from root_c, it should remain in root_c"
5994 );
5995}
5996
5997fn toggle_expand_dir(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
5998 let path = rel_path(path);
5999 panel.update_in(cx, |panel, window, cx| {
6000 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
6001 let worktree = worktree.read(cx);
6002 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
6003 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
6004 panel.toggle_expanded(entry_id, window, cx);
6005 return;
6006 }
6007 }
6008 panic!("no worktree for path {:?}", path);
6009 });
6010 cx.run_until_parked();
6011}
6012
6013#[gpui::test]
6014async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) {
6015 init_test_with_editor(cx);
6016
6017 let fs = FakeFs::new(cx.executor());
6018 fs.insert_tree(
6019 path!("/root"),
6020 json!({
6021 ".gitignore": "**/ignored_dir\n**/ignored_nested",
6022 "dir1": {
6023 "empty1": {
6024 "empty2": {
6025 "empty3": {
6026 "file.txt": ""
6027 }
6028 }
6029 },
6030 "subdir1": {
6031 "file1.txt": "",
6032 "file2.txt": "",
6033 "ignored_nested": {
6034 "ignored_file.txt": ""
6035 }
6036 },
6037 "ignored_dir": {
6038 "subdir": {
6039 "deep_file.txt": ""
6040 }
6041 }
6042 }
6043 }),
6044 )
6045 .await;
6046
6047 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
6048 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6049 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6050
6051 // Test 1: When auto-fold is enabled
6052 cx.update(|_, cx| {
6053 let settings = *ProjectPanelSettings::get_global(cx);
6054 ProjectPanelSettings::override_global(
6055 ProjectPanelSettings {
6056 auto_fold_dirs: true,
6057 ..settings
6058 },
6059 cx,
6060 );
6061 });
6062
6063 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6064 cx.run_until_parked();
6065
6066 assert_eq!(
6067 visible_entries_as_strings(&panel, 0..20, cx),
6068 &["v root", " > dir1", " .gitignore",],
6069 "Initial state should show collapsed root structure"
6070 );
6071
6072 toggle_expand_dir(&panel, "root/dir1", cx);
6073 assert_eq!(
6074 visible_entries_as_strings(&panel, 0..20, cx),
6075 &[
6076 "v root",
6077 " v dir1 <== selected",
6078 " > empty1/empty2/empty3",
6079 " > ignored_dir",
6080 " > subdir1",
6081 " .gitignore",
6082 ],
6083 "Should show first level with auto-folded dirs and ignored dir visible"
6084 );
6085
6086 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
6087 panel.update_in(cx, |panel, window, cx| {
6088 let project = panel.project.read(cx);
6089 let worktree = project.worktrees(cx).next().unwrap().read(cx);
6090 panel.expand_all_for_entry(worktree.id(), entry_id, cx);
6091 panel.update_visible_entries(None, false, false, window, cx);
6092 });
6093 cx.run_until_parked();
6094
6095 assert_eq!(
6096 visible_entries_as_strings(&panel, 0..20, cx),
6097 &[
6098 "v root",
6099 " v dir1 <== selected",
6100 " v empty1",
6101 " v empty2",
6102 " v empty3",
6103 " file.txt",
6104 " > ignored_dir",
6105 " v subdir1",
6106 " > ignored_nested",
6107 " file1.txt",
6108 " file2.txt",
6109 " .gitignore",
6110 ],
6111 "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested"
6112 );
6113
6114 // Test 2: When auto-fold is disabled
6115 cx.update(|_, cx| {
6116 let settings = *ProjectPanelSettings::get_global(cx);
6117 ProjectPanelSettings::override_global(
6118 ProjectPanelSettings {
6119 auto_fold_dirs: false,
6120 ..settings
6121 },
6122 cx,
6123 );
6124 });
6125
6126 panel.update_in(cx, |panel, window, cx| {
6127 panel.collapse_all_entries(&CollapseAllEntries, window, cx);
6128 });
6129
6130 toggle_expand_dir(&panel, "root/dir1", cx);
6131 assert_eq!(
6132 visible_entries_as_strings(&panel, 0..20, cx),
6133 &[
6134 "v root",
6135 " v dir1 <== selected",
6136 " > empty1",
6137 " > ignored_dir",
6138 " > subdir1",
6139 " .gitignore",
6140 ],
6141 "With auto-fold disabled: should show all directories separately"
6142 );
6143
6144 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
6145 panel.update_in(cx, |panel, window, cx| {
6146 let project = panel.project.read(cx);
6147 let worktree = project.worktrees(cx).next().unwrap().read(cx);
6148 panel.expand_all_for_entry(worktree.id(), entry_id, cx);
6149 panel.update_visible_entries(None, false, false, window, cx);
6150 });
6151 cx.run_until_parked();
6152
6153 assert_eq!(
6154 visible_entries_as_strings(&panel, 0..20, cx),
6155 &[
6156 "v root",
6157 " v dir1 <== selected",
6158 " v empty1",
6159 " v empty2",
6160 " v empty3",
6161 " file.txt",
6162 " > ignored_dir",
6163 " v subdir1",
6164 " > ignored_nested",
6165 " file1.txt",
6166 " file2.txt",
6167 " .gitignore",
6168 ],
6169 "After expand_all without auto-fold: should expand all dirs normally, \
6170 expand ignored_dir itself but not its subdirs, and not expand ignored_nested"
6171 );
6172
6173 // Test 3: When explicitly called on ignored directory
6174 let ignored_dir_entry = find_project_entry(&panel, "root/dir1/ignored_dir", cx).unwrap();
6175 panel.update_in(cx, |panel, window, cx| {
6176 let project = panel.project.read(cx);
6177 let worktree = project.worktrees(cx).next().unwrap().read(cx);
6178 panel.expand_all_for_entry(worktree.id(), ignored_dir_entry, cx);
6179 panel.update_visible_entries(None, false, false, window, cx);
6180 });
6181 cx.run_until_parked();
6182
6183 assert_eq!(
6184 visible_entries_as_strings(&panel, 0..20, cx),
6185 &[
6186 "v root",
6187 " v dir1 <== selected",
6188 " v empty1",
6189 " v empty2",
6190 " v empty3",
6191 " file.txt",
6192 " v ignored_dir",
6193 " v subdir",
6194 " deep_file.txt",
6195 " v subdir1",
6196 " > ignored_nested",
6197 " file1.txt",
6198 " file2.txt",
6199 " .gitignore",
6200 ],
6201 "After expand_all on ignored_dir: should expand all contents of the ignored directory"
6202 );
6203}
6204
6205#[gpui::test]
6206async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) {
6207 init_test(cx);
6208
6209 let fs = FakeFs::new(cx.executor());
6210 fs.insert_tree(
6211 path!("/root"),
6212 json!({
6213 "dir1": {
6214 "subdir1": {
6215 "nested1": {
6216 "file1.txt": "",
6217 "file2.txt": ""
6218 },
6219 },
6220 "subdir2": {
6221 "file4.txt": ""
6222 }
6223 },
6224 "dir2": {
6225 "single_file": {
6226 "file5.txt": ""
6227 }
6228 }
6229 }),
6230 )
6231 .await;
6232
6233 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
6234 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6235 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6236
6237 // Test 1: Basic collapsing
6238 {
6239 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6240 cx.run_until_parked();
6241
6242 toggle_expand_dir(&panel, "root/dir1", cx);
6243 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
6244 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
6245 toggle_expand_dir(&panel, "root/dir1/subdir2", cx);
6246
6247 assert_eq!(
6248 visible_entries_as_strings(&panel, 0..20, cx),
6249 &[
6250 "v root",
6251 " v dir1",
6252 " v subdir1",
6253 " v nested1",
6254 " file1.txt",
6255 " file2.txt",
6256 " v subdir2 <== selected",
6257 " file4.txt",
6258 " > dir2",
6259 ],
6260 "Initial state with everything expanded"
6261 );
6262
6263 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
6264 panel.update_in(cx, |panel, window, cx| {
6265 let project = panel.project.read(cx);
6266 let worktree = project.worktrees(cx).next().unwrap().read(cx);
6267 panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
6268 panel.update_visible_entries(None, false, false, window, cx);
6269 });
6270 cx.run_until_parked();
6271
6272 assert_eq!(
6273 visible_entries_as_strings(&panel, 0..20, cx),
6274 &["v root", " > dir1", " > dir2",],
6275 "All subdirs under dir1 should be collapsed"
6276 );
6277 }
6278
6279 // Test 2: With auto-fold enabled
6280 {
6281 cx.update(|_, cx| {
6282 let settings = *ProjectPanelSettings::get_global(cx);
6283 ProjectPanelSettings::override_global(
6284 ProjectPanelSettings {
6285 auto_fold_dirs: true,
6286 ..settings
6287 },
6288 cx,
6289 );
6290 });
6291
6292 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6293 cx.run_until_parked();
6294
6295 toggle_expand_dir(&panel, "root/dir1", cx);
6296 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
6297 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
6298
6299 assert_eq!(
6300 visible_entries_as_strings(&panel, 0..20, cx),
6301 &[
6302 "v root",
6303 " v dir1",
6304 " v subdir1/nested1 <== selected",
6305 " file1.txt",
6306 " file2.txt",
6307 " > subdir2",
6308 " > dir2/single_file",
6309 ],
6310 "Initial state with some dirs expanded"
6311 );
6312
6313 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
6314 panel.update(cx, |panel, 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 });
6319
6320 toggle_expand_dir(&panel, "root/dir1", cx);
6321
6322 assert_eq!(
6323 visible_entries_as_strings(&panel, 0..20, cx),
6324 &[
6325 "v root",
6326 " v dir1 <== selected",
6327 " > subdir1/nested1",
6328 " > subdir2",
6329 " > dir2/single_file",
6330 ],
6331 "Subdirs should be collapsed and folded with auto-fold enabled"
6332 );
6333 }
6334
6335 // Test 3: With auto-fold disabled
6336 {
6337 cx.update(|_, cx| {
6338 let settings = *ProjectPanelSettings::get_global(cx);
6339 ProjectPanelSettings::override_global(
6340 ProjectPanelSettings {
6341 auto_fold_dirs: false,
6342 ..settings
6343 },
6344 cx,
6345 );
6346 });
6347
6348 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6349 cx.run_until_parked();
6350
6351 toggle_expand_dir(&panel, "root/dir1", cx);
6352 toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
6353 toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx);
6354
6355 assert_eq!(
6356 visible_entries_as_strings(&panel, 0..20, cx),
6357 &[
6358 "v root",
6359 " v dir1",
6360 " v subdir1",
6361 " v nested1 <== selected",
6362 " file1.txt",
6363 " file2.txt",
6364 " > subdir2",
6365 " > dir2",
6366 ],
6367 "Initial state with some dirs expanded and auto-fold disabled"
6368 );
6369
6370 let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap();
6371 panel.update(cx, |panel, cx| {
6372 let project = panel.project.read(cx);
6373 let worktree = project.worktrees(cx).next().unwrap().read(cx);
6374 panel.collapse_all_for_entry(worktree.id(), entry_id, cx);
6375 });
6376
6377 toggle_expand_dir(&panel, "root/dir1", cx);
6378
6379 assert_eq!(
6380 visible_entries_as_strings(&panel, 0..20, cx),
6381 &[
6382 "v root",
6383 " v dir1 <== selected",
6384 " > subdir1",
6385 " > subdir2",
6386 " > dir2",
6387 ],
6388 "Subdirs should be collapsed but not folded with auto-fold disabled"
6389 );
6390 }
6391}
6392
6393#[gpui::test]
6394async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) {
6395 init_test(cx);
6396
6397 let fs = FakeFs::new(cx.executor());
6398 fs.insert_tree(
6399 path!("/root"),
6400 json!({
6401 "dir1": {
6402 "file1.txt": "",
6403 },
6404 }),
6405 )
6406 .await;
6407
6408 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
6409 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6410 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6411
6412 let panel = workspace
6413 .update(cx, |workspace, window, cx| {
6414 let panel = ProjectPanel::new(workspace, window, cx);
6415 workspace.add_panel(panel.clone(), window, cx);
6416 panel
6417 })
6418 .unwrap();
6419 cx.run_until_parked();
6420
6421 #[rustfmt::skip]
6422 assert_eq!(
6423 visible_entries_as_strings(&panel, 0..20, cx),
6424 &[
6425 "v root",
6426 " > dir1",
6427 ],
6428 "Initial state with nothing selected"
6429 );
6430
6431 panel.update_in(cx, |panel, window, cx| {
6432 panel.new_file(&NewFile, window, cx);
6433 });
6434 cx.run_until_parked();
6435 panel.update_in(cx, |panel, window, cx| {
6436 assert!(panel.filename_editor.read(cx).is_focused(window));
6437 });
6438 panel
6439 .update_in(cx, |panel, window, cx| {
6440 panel.filename_editor.update(cx, |editor, cx| {
6441 editor.set_text("hello_from_no_selections", window, cx)
6442 });
6443 panel.confirm_edit(true, window, cx).unwrap()
6444 })
6445 .await
6446 .unwrap();
6447 cx.run_until_parked();
6448 #[rustfmt::skip]
6449 assert_eq!(
6450 visible_entries_as_strings(&panel, 0..20, cx),
6451 &[
6452 "v root",
6453 " > dir1",
6454 " hello_from_no_selections <== selected <== marked",
6455 ],
6456 "A new file is created under the root directory"
6457 );
6458}
6459
6460#[gpui::test]
6461async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppContext) {
6462 init_test(cx);
6463
6464 let fs = FakeFs::new(cx.executor());
6465 fs.insert_tree(
6466 path!("/root"),
6467 json!({
6468 "existing_dir": {
6469 "existing_file.txt": "",
6470 },
6471 "existing_file.txt": "",
6472 }),
6473 )
6474 .await;
6475
6476 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
6477 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6478 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6479
6480 cx.update(|_, cx| {
6481 let settings = *ProjectPanelSettings::get_global(cx);
6482 ProjectPanelSettings::override_global(
6483 ProjectPanelSettings {
6484 hide_root: true,
6485 ..settings
6486 },
6487 cx,
6488 );
6489 });
6490
6491 let panel = workspace
6492 .update(cx, |workspace, window, cx| {
6493 let panel = ProjectPanel::new(workspace, window, cx);
6494 workspace.add_panel(panel.clone(), window, cx);
6495 panel
6496 })
6497 .unwrap();
6498 cx.run_until_parked();
6499
6500 #[rustfmt::skip]
6501 assert_eq!(
6502 visible_entries_as_strings(&panel, 0..20, cx),
6503 &[
6504 "> existing_dir",
6505 " existing_file.txt",
6506 ],
6507 "Initial state with hide_root=true, root should be hidden and nothing selected"
6508 );
6509
6510 panel.update(cx, |panel, _| {
6511 assert!(
6512 panel.state.selection.is_none(),
6513 "Should have no selection initially"
6514 );
6515 });
6516
6517 // Test 1: Create new file when no entry is selected
6518 panel.update_in(cx, |panel, window, cx| {
6519 panel.new_file(&NewFile, window, cx);
6520 });
6521 cx.run_until_parked();
6522 panel.update_in(cx, |panel, window, cx| {
6523 assert!(panel.filename_editor.read(cx).is_focused(window));
6524 });
6525 cx.run_until_parked();
6526 #[rustfmt::skip]
6527 assert_eq!(
6528 visible_entries_as_strings(&panel, 0..20, cx),
6529 &[
6530 "> existing_dir",
6531 " [EDITOR: ''] <== selected",
6532 " existing_file.txt",
6533 ],
6534 "Editor should appear at root level when hide_root=true and no selection"
6535 );
6536
6537 let confirm = panel.update_in(cx, |panel, window, cx| {
6538 panel.filename_editor.update(cx, |editor, cx| {
6539 editor.set_text("new_file_at_root.txt", window, cx)
6540 });
6541 panel.confirm_edit(true, window, cx).unwrap()
6542 });
6543 confirm.await.unwrap();
6544 cx.run_until_parked();
6545
6546 #[rustfmt::skip]
6547 assert_eq!(
6548 visible_entries_as_strings(&panel, 0..20, cx),
6549 &[
6550 "> existing_dir",
6551 " existing_file.txt",
6552 " new_file_at_root.txt <== selected <== marked",
6553 ],
6554 "New file should be created at root level and visible without root prefix"
6555 );
6556
6557 assert!(
6558 fs.is_file(Path::new("/root/new_file_at_root.txt")).await,
6559 "File should be created in the actual root directory"
6560 );
6561
6562 // Test 2: Create new directory when no entry is selected
6563 panel.update(cx, |panel, _| {
6564 panel.state.selection = None;
6565 });
6566
6567 panel.update_in(cx, |panel, window, cx| {
6568 panel.new_directory(&NewDirectory, window, cx);
6569 });
6570 cx.run_until_parked();
6571
6572 panel.update_in(cx, |panel, window, cx| {
6573 assert!(panel.filename_editor.read(cx).is_focused(window));
6574 });
6575
6576 #[rustfmt::skip]
6577 assert_eq!(
6578 visible_entries_as_strings(&panel, 0..20, cx),
6579 &[
6580 "> [EDITOR: ''] <== selected",
6581 "> existing_dir",
6582 " existing_file.txt",
6583 " new_file_at_root.txt",
6584 ],
6585 "Directory editor should appear at root level when hide_root=true and no selection"
6586 );
6587
6588 let confirm = panel.update_in(cx, |panel, window, cx| {
6589 panel.filename_editor.update(cx, |editor, cx| {
6590 editor.set_text("new_dir_at_root", window, cx)
6591 });
6592 panel.confirm_edit(true, window, cx).unwrap()
6593 });
6594 confirm.await.unwrap();
6595 cx.run_until_parked();
6596
6597 #[rustfmt::skip]
6598 assert_eq!(
6599 visible_entries_as_strings(&panel, 0..20, cx),
6600 &[
6601 "> existing_dir",
6602 "v new_dir_at_root <== selected",
6603 " existing_file.txt",
6604 " new_file_at_root.txt",
6605 ],
6606 "New directory should be created at root level and visible without root prefix"
6607 );
6608
6609 assert!(
6610 fs.is_dir(Path::new("/root/new_dir_at_root")).await,
6611 "Directory should be created in the actual root directory"
6612 );
6613}
6614
6615#[gpui::test]
6616async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) {
6617 init_test(cx);
6618
6619 let fs = FakeFs::new(cx.executor());
6620 fs.insert_tree(
6621 "/root",
6622 json!({
6623 "dir1": {
6624 "file1.txt": "",
6625 "dir2": {
6626 "file2.txt": ""
6627 }
6628 },
6629 "file3.txt": ""
6630 }),
6631 )
6632 .await;
6633
6634 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
6635 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6636 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6637 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6638 cx.run_until_parked();
6639
6640 panel.update(cx, |panel, cx| {
6641 let project = panel.project.read(cx);
6642 let worktree = project.visible_worktrees(cx).next().unwrap();
6643 let worktree = worktree.read(cx);
6644
6645 // Test 1: Target is a directory, should highlight the directory itself
6646 let dir_entry = worktree.entry_for_path(rel_path("dir1")).unwrap();
6647 let result = panel.highlight_entry_for_external_drag(dir_entry, worktree);
6648 assert_eq!(
6649 result,
6650 Some(dir_entry.id),
6651 "Should highlight directory itself"
6652 );
6653
6654 // Test 2: Target is nested file, should highlight immediate parent
6655 let nested_file = worktree
6656 .entry_for_path(rel_path("dir1/dir2/file2.txt"))
6657 .unwrap();
6658 let nested_parent = worktree.entry_for_path(rel_path("dir1/dir2")).unwrap();
6659 let result = panel.highlight_entry_for_external_drag(nested_file, worktree);
6660 assert_eq!(
6661 result,
6662 Some(nested_parent.id),
6663 "Should highlight immediate parent"
6664 );
6665
6666 // Test 3: Target is root level file, should highlight root
6667 let root_file = worktree.entry_for_path(rel_path("file3.txt")).unwrap();
6668 let result = panel.highlight_entry_for_external_drag(root_file, worktree);
6669 assert_eq!(
6670 result,
6671 Some(worktree.root_entry().unwrap().id),
6672 "Root level file should return None"
6673 );
6674
6675 // Test 4: Target is root itself, should highlight root
6676 let root_entry = worktree.root_entry().unwrap();
6677 let result = panel.highlight_entry_for_external_drag(root_entry, worktree);
6678 assert_eq!(
6679 result,
6680 Some(root_entry.id),
6681 "Root level file should return None"
6682 );
6683 });
6684}
6685
6686#[gpui::test]
6687async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) {
6688 init_test(cx);
6689
6690 let fs = FakeFs::new(cx.executor());
6691 fs.insert_tree(
6692 "/root",
6693 json!({
6694 "parent_dir": {
6695 "child_file.txt": "",
6696 "sibling_file.txt": "",
6697 "child_dir": {
6698 "nested_file.txt": ""
6699 }
6700 },
6701 "other_dir": {
6702 "other_file.txt": ""
6703 }
6704 }),
6705 )
6706 .await;
6707
6708 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
6709 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6710 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6711 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6712 cx.run_until_parked();
6713
6714 panel.update(cx, |panel, cx| {
6715 let project = panel.project.read(cx);
6716 let worktree = project.visible_worktrees(cx).next().unwrap();
6717 let worktree_id = worktree.read(cx).id();
6718 let worktree = worktree.read(cx);
6719
6720 let parent_dir = worktree.entry_for_path(rel_path("parent_dir")).unwrap();
6721 let child_file = worktree
6722 .entry_for_path(rel_path("parent_dir/child_file.txt"))
6723 .unwrap();
6724 let sibling_file = worktree
6725 .entry_for_path(rel_path("parent_dir/sibling_file.txt"))
6726 .unwrap();
6727 let child_dir = worktree
6728 .entry_for_path(rel_path("parent_dir/child_dir"))
6729 .unwrap();
6730 let other_dir = worktree.entry_for_path(rel_path("other_dir")).unwrap();
6731 let other_file = worktree
6732 .entry_for_path(rel_path("other_dir/other_file.txt"))
6733 .unwrap();
6734
6735 // Test 1: Single item drag, don't highlight parent directory
6736 let dragged_selection = DraggedSelection {
6737 active_selection: SelectedEntry {
6738 worktree_id,
6739 entry_id: child_file.id,
6740 },
6741 marked_selections: Arc::new([SelectedEntry {
6742 worktree_id,
6743 entry_id: child_file.id,
6744 }]),
6745 };
6746 let result =
6747 panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
6748 assert_eq!(result, None, "Should not highlight parent of dragged item");
6749
6750 // Test 2: Single item drag, don't highlight sibling files
6751 let result = panel.highlight_entry_for_selection_drag(
6752 sibling_file,
6753 worktree,
6754 &dragged_selection,
6755 cx,
6756 );
6757 assert_eq!(result, None, "Should not highlight sibling files");
6758
6759 // Test 3: Single item drag, highlight unrelated directory
6760 let result =
6761 panel.highlight_entry_for_selection_drag(other_dir, worktree, &dragged_selection, cx);
6762 assert_eq!(
6763 result,
6764 Some(other_dir.id),
6765 "Should highlight unrelated directory"
6766 );
6767
6768 // Test 4: Single item drag, highlight sibling directory
6769 let result =
6770 panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
6771 assert_eq!(
6772 result,
6773 Some(child_dir.id),
6774 "Should highlight sibling directory"
6775 );
6776
6777 // Test 5: Multiple items drag, highlight parent directory
6778 let dragged_selection = DraggedSelection {
6779 active_selection: SelectedEntry {
6780 worktree_id,
6781 entry_id: child_file.id,
6782 },
6783 marked_selections: Arc::new([
6784 SelectedEntry {
6785 worktree_id,
6786 entry_id: child_file.id,
6787 },
6788 SelectedEntry {
6789 worktree_id,
6790 entry_id: sibling_file.id,
6791 },
6792 ]),
6793 };
6794 let result =
6795 panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
6796 assert_eq!(
6797 result,
6798 Some(parent_dir.id),
6799 "Should highlight parent with multiple items"
6800 );
6801
6802 // Test 6: Target is file in different directory, highlight parent
6803 let result =
6804 panel.highlight_entry_for_selection_drag(other_file, worktree, &dragged_selection, cx);
6805 assert_eq!(
6806 result,
6807 Some(other_dir.id),
6808 "Should highlight parent of target file"
6809 );
6810
6811 // Test 7: Target is directory, always highlight
6812 let result =
6813 panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
6814 assert_eq!(
6815 result,
6816 Some(child_dir.id),
6817 "Should always highlight directories"
6818 );
6819 });
6820}
6821
6822#[gpui::test]
6823async fn test_highlight_entry_for_selection_drag_cross_worktree(cx: &mut gpui::TestAppContext) {
6824 init_test(cx);
6825
6826 let fs = FakeFs::new(cx.executor());
6827 fs.insert_tree(
6828 "/root1",
6829 json!({
6830 "src": {
6831 "main.rs": "",
6832 "lib.rs": ""
6833 }
6834 }),
6835 )
6836 .await;
6837 fs.insert_tree(
6838 "/root2",
6839 json!({
6840 "src": {
6841 "main.rs": "",
6842 "test.rs": ""
6843 }
6844 }),
6845 )
6846 .await;
6847
6848 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
6849 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6850 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6851 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6852 cx.run_until_parked();
6853
6854 panel.update(cx, |panel, cx| {
6855 let project = panel.project.read(cx);
6856 let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
6857
6858 let worktree_a = &worktrees[0];
6859 let main_rs_from_a = worktree_a
6860 .read(cx)
6861 .entry_for_path(rel_path("src/main.rs"))
6862 .unwrap();
6863
6864 let worktree_b = &worktrees[1];
6865 let src_dir_from_b = worktree_b.read(cx).entry_for_path(rel_path("src")).unwrap();
6866 let main_rs_from_b = worktree_b
6867 .read(cx)
6868 .entry_for_path(rel_path("src/main.rs"))
6869 .unwrap();
6870
6871 // Test dragging file from worktree A onto parent of file with same relative path in worktree B
6872 let dragged_selection = DraggedSelection {
6873 active_selection: SelectedEntry {
6874 worktree_id: worktree_a.read(cx).id(),
6875 entry_id: main_rs_from_a.id,
6876 },
6877 marked_selections: Arc::new([SelectedEntry {
6878 worktree_id: worktree_a.read(cx).id(),
6879 entry_id: main_rs_from_a.id,
6880 }]),
6881 };
6882
6883 let result = panel.highlight_entry_for_selection_drag(
6884 src_dir_from_b,
6885 worktree_b.read(cx),
6886 &dragged_selection,
6887 cx,
6888 );
6889 assert_eq!(
6890 result,
6891 Some(src_dir_from_b.id),
6892 "Should highlight target directory from different worktree even with same relative path"
6893 );
6894
6895 // Test dragging file from worktree A onto file with same relative path in worktree B
6896 let result = panel.highlight_entry_for_selection_drag(
6897 main_rs_from_b,
6898 worktree_b.read(cx),
6899 &dragged_selection,
6900 cx,
6901 );
6902 assert_eq!(
6903 result,
6904 Some(src_dir_from_b.id),
6905 "Should highlight parent of target file from different worktree"
6906 );
6907 });
6908}
6909
6910#[gpui::test]
6911async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::TestAppContext) {
6912 init_test(cx);
6913
6914 let fs = FakeFs::new(cx.executor());
6915 fs.insert_tree(
6916 "/root1",
6917 json!({
6918 "parent_dir": {
6919 "child_file.txt": "",
6920 "nested_dir": {
6921 "nested_file.txt": ""
6922 }
6923 },
6924 "root_file.txt": ""
6925 }),
6926 )
6927 .await;
6928
6929 fs.insert_tree(
6930 "/root2",
6931 json!({
6932 "other_dir": {
6933 "other_file.txt": ""
6934 }
6935 }),
6936 )
6937 .await;
6938
6939 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
6940 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6941 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6942 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
6943 cx.run_until_parked();
6944
6945 panel.update(cx, |panel, cx| {
6946 let project = panel.project.read(cx);
6947 let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
6948 let worktree1 = worktrees[0].read(cx);
6949 let worktree2 = worktrees[1].read(cx);
6950 let worktree1_id = worktree1.id();
6951 let _worktree2_id = worktree2.id();
6952
6953 let root1_entry = worktree1.root_entry().unwrap();
6954 let root2_entry = worktree2.root_entry().unwrap();
6955 let _parent_dir = worktree1.entry_for_path(rel_path("parent_dir")).unwrap();
6956 let child_file = worktree1
6957 .entry_for_path(rel_path("parent_dir/child_file.txt"))
6958 .unwrap();
6959 let nested_file = worktree1
6960 .entry_for_path(rel_path("parent_dir/nested_dir/nested_file.txt"))
6961 .unwrap();
6962 let root_file = worktree1.entry_for_path(rel_path("root_file.txt")).unwrap();
6963
6964 // Test 1: Multiple entries - should always highlight background
6965 let multiple_dragged_selection = DraggedSelection {
6966 active_selection: SelectedEntry {
6967 worktree_id: worktree1_id,
6968 entry_id: child_file.id,
6969 },
6970 marked_selections: Arc::new([
6971 SelectedEntry {
6972 worktree_id: worktree1_id,
6973 entry_id: child_file.id,
6974 },
6975 SelectedEntry {
6976 worktree_id: worktree1_id,
6977 entry_id: nested_file.id,
6978 },
6979 ]),
6980 };
6981
6982 let result = panel.should_highlight_background_for_selection_drag(
6983 &multiple_dragged_selection,
6984 root1_entry.id,
6985 cx,
6986 );
6987 assert!(result, "Should highlight background for multiple entries");
6988
6989 // Test 2: Single entry with non-empty parent path - should highlight background
6990 let nested_dragged_selection = DraggedSelection {
6991 active_selection: SelectedEntry {
6992 worktree_id: worktree1_id,
6993 entry_id: nested_file.id,
6994 },
6995 marked_selections: Arc::new([SelectedEntry {
6996 worktree_id: worktree1_id,
6997 entry_id: nested_file.id,
6998 }]),
6999 };
7000
7001 let result = panel.should_highlight_background_for_selection_drag(
7002 &nested_dragged_selection,
7003 root1_entry.id,
7004 cx,
7005 );
7006 assert!(result, "Should highlight background for nested file");
7007
7008 // Test 3: Single entry at root level, same worktree - should NOT highlight background
7009 let root_file_dragged_selection = DraggedSelection {
7010 active_selection: SelectedEntry {
7011 worktree_id: worktree1_id,
7012 entry_id: root_file.id,
7013 },
7014 marked_selections: Arc::new([SelectedEntry {
7015 worktree_id: worktree1_id,
7016 entry_id: root_file.id,
7017 }]),
7018 };
7019
7020 let result = panel.should_highlight_background_for_selection_drag(
7021 &root_file_dragged_selection,
7022 root1_entry.id,
7023 cx,
7024 );
7025 assert!(
7026 !result,
7027 "Should NOT highlight background for root file in same worktree"
7028 );
7029
7030 // Test 4: Single entry at root level, different worktree - should highlight background
7031 let result = panel.should_highlight_background_for_selection_drag(
7032 &root_file_dragged_selection,
7033 root2_entry.id,
7034 cx,
7035 );
7036 assert!(
7037 result,
7038 "Should highlight background for root file from different worktree"
7039 );
7040
7041 // Test 5: Single entry in subdirectory - should highlight background
7042 let child_file_dragged_selection = DraggedSelection {
7043 active_selection: SelectedEntry {
7044 worktree_id: worktree1_id,
7045 entry_id: child_file.id,
7046 },
7047 marked_selections: Arc::new([SelectedEntry {
7048 worktree_id: worktree1_id,
7049 entry_id: child_file.id,
7050 }]),
7051 };
7052
7053 let result = panel.should_highlight_background_for_selection_drag(
7054 &child_file_dragged_selection,
7055 root1_entry.id,
7056 cx,
7057 );
7058 assert!(
7059 result,
7060 "Should highlight background for file with non-empty parent path"
7061 );
7062 });
7063}
7064
7065#[gpui::test]
7066async fn test_hide_root(cx: &mut gpui::TestAppContext) {
7067 init_test(cx);
7068
7069 let fs = FakeFs::new(cx.executor());
7070 fs.insert_tree(
7071 "/root1",
7072 json!({
7073 "dir1": {
7074 "file1.txt": "content",
7075 "file2.txt": "content",
7076 },
7077 "dir2": {
7078 "file3.txt": "content",
7079 },
7080 "file4.txt": "content",
7081 }),
7082 )
7083 .await;
7084
7085 fs.insert_tree(
7086 "/root2",
7087 json!({
7088 "dir3": {
7089 "file5.txt": "content",
7090 },
7091 "file6.txt": "content",
7092 }),
7093 )
7094 .await;
7095
7096 // Test 1: Single worktree with hide_root = false
7097 {
7098 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
7099 let workspace =
7100 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7101 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7102
7103 cx.update(|_, cx| {
7104 let settings = *ProjectPanelSettings::get_global(cx);
7105 ProjectPanelSettings::override_global(
7106 ProjectPanelSettings {
7107 hide_root: false,
7108 ..settings
7109 },
7110 cx,
7111 );
7112 });
7113
7114 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7115 cx.run_until_parked();
7116
7117 #[rustfmt::skip]
7118 assert_eq!(
7119 visible_entries_as_strings(&panel, 0..10, cx),
7120 &[
7121 "v root1",
7122 " > dir1",
7123 " > dir2",
7124 " file4.txt",
7125 ],
7126 "With hide_root=false and single worktree, root should be visible"
7127 );
7128 }
7129
7130 // Test 2: Single worktree with hide_root = true
7131 {
7132 let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
7133 let workspace =
7134 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7135 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7136
7137 // Set hide_root to true
7138 cx.update(|_, cx| {
7139 let settings = *ProjectPanelSettings::get_global(cx);
7140 ProjectPanelSettings::override_global(
7141 ProjectPanelSettings {
7142 hide_root: true,
7143 ..settings
7144 },
7145 cx,
7146 );
7147 });
7148
7149 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7150 cx.run_until_parked();
7151
7152 assert_eq!(
7153 visible_entries_as_strings(&panel, 0..10, cx),
7154 &["> dir1", "> dir2", " file4.txt",],
7155 "With hide_root=true and single worktree, root should be hidden"
7156 );
7157
7158 // Test expanding directories still works without root
7159 toggle_expand_dir(&panel, "root1/dir1", cx);
7160 assert_eq!(
7161 visible_entries_as_strings(&panel, 0..10, cx),
7162 &[
7163 "v dir1 <== selected",
7164 " file1.txt",
7165 " file2.txt",
7166 "> dir2",
7167 " file4.txt",
7168 ],
7169 "Should be able to expand directories even when root is hidden"
7170 );
7171 }
7172
7173 // Test 3: Multiple worktrees with hide_root = true
7174 {
7175 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
7176 let workspace =
7177 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7178 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7179
7180 // Set hide_root to true
7181 cx.update(|_, cx| {
7182 let settings = *ProjectPanelSettings::get_global(cx);
7183 ProjectPanelSettings::override_global(
7184 ProjectPanelSettings {
7185 hide_root: true,
7186 ..settings
7187 },
7188 cx,
7189 );
7190 });
7191
7192 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7193 cx.run_until_parked();
7194
7195 assert_eq!(
7196 visible_entries_as_strings(&panel, 0..10, cx),
7197 &[
7198 "v root1",
7199 " > dir1",
7200 " > dir2",
7201 " file4.txt",
7202 "v root2",
7203 " > dir3",
7204 " file6.txt",
7205 ],
7206 "With hide_root=true and multiple worktrees, roots should still be visible"
7207 );
7208 }
7209
7210 // Test 4: Multiple worktrees with hide_root = false
7211 {
7212 let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
7213 let workspace =
7214 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7215 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7216
7217 cx.update(|_, cx| {
7218 let settings = *ProjectPanelSettings::get_global(cx);
7219 ProjectPanelSettings::override_global(
7220 ProjectPanelSettings {
7221 hide_root: false,
7222 ..settings
7223 },
7224 cx,
7225 );
7226 });
7227
7228 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7229 cx.run_until_parked();
7230
7231 assert_eq!(
7232 visible_entries_as_strings(&panel, 0..10, cx),
7233 &[
7234 "v root1",
7235 " > dir1",
7236 " > dir2",
7237 " file4.txt",
7238 "v root2",
7239 " > dir3",
7240 " file6.txt",
7241 ],
7242 "With hide_root=false and multiple worktrees, roots should be visible"
7243 );
7244 }
7245}
7246
7247#[gpui::test]
7248async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) {
7249 init_test_with_editor(cx);
7250
7251 let fs = FakeFs::new(cx.executor());
7252 fs.insert_tree(
7253 "/root",
7254 json!({
7255 "file1.txt": "content of file1",
7256 "file2.txt": "content of file2",
7257 "dir1": {
7258 "file3.txt": "content of file3"
7259 }
7260 }),
7261 )
7262 .await;
7263
7264 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7265 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7266 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7267 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7268 cx.run_until_parked();
7269
7270 let file1_path = "root/file1.txt";
7271 let file2_path = "root/file2.txt";
7272 select_path_with_mark(&panel, file1_path, cx);
7273 select_path_with_mark(&panel, file2_path, cx);
7274
7275 panel.update_in(cx, |panel, window, cx| {
7276 panel.compare_marked_files(&CompareMarkedFiles, window, cx);
7277 });
7278 cx.executor().run_until_parked();
7279
7280 workspace
7281 .update(cx, |workspace, _, cx| {
7282 let active_items = workspace
7283 .panes()
7284 .iter()
7285 .filter_map(|pane| pane.read(cx).active_item())
7286 .collect::<Vec<_>>();
7287 assert_eq!(active_items.len(), 1);
7288 let diff_view = active_items
7289 .into_iter()
7290 .next()
7291 .unwrap()
7292 .downcast::<FileDiffView>()
7293 .expect("Open item should be an FileDiffView");
7294 assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt");
7295 assert_eq!(
7296 diff_view.tab_tooltip_text(cx).unwrap(),
7297 format!(
7298 "{} ↔ {}",
7299 rel_path(file1_path).display(PathStyle::local()),
7300 rel_path(file2_path).display(PathStyle::local())
7301 )
7302 );
7303 })
7304 .unwrap();
7305
7306 let file1_entry_id = find_project_entry(&panel, file1_path, cx).unwrap();
7307 let file2_entry_id = find_project_entry(&panel, file2_path, cx).unwrap();
7308 let worktree_id = panel.update(cx, |panel, cx| {
7309 panel
7310 .project
7311 .read(cx)
7312 .worktrees(cx)
7313 .next()
7314 .unwrap()
7315 .read(cx)
7316 .id()
7317 });
7318
7319 let expected_entries = [
7320 SelectedEntry {
7321 worktree_id,
7322 entry_id: file1_entry_id,
7323 },
7324 SelectedEntry {
7325 worktree_id,
7326 entry_id: file2_entry_id,
7327 },
7328 ];
7329 panel.update(cx, |panel, _cx| {
7330 assert_eq!(
7331 &panel.marked_entries, &expected_entries,
7332 "Should keep marked entries after comparison"
7333 );
7334 });
7335
7336 panel.update(cx, |panel, cx| {
7337 panel.project.update(cx, |_, cx| {
7338 cx.emit(project::Event::RevealInProjectPanel(file2_entry_id))
7339 })
7340 });
7341
7342 panel.update(cx, |panel, _cx| {
7343 assert_eq!(
7344 &panel.marked_entries, &expected_entries,
7345 "Marked entries should persist after focusing back on the project panel"
7346 );
7347 });
7348}
7349
7350#[gpui::test]
7351async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) {
7352 init_test_with_editor(cx);
7353
7354 let fs = FakeFs::new(cx.executor());
7355 fs.insert_tree(
7356 "/root",
7357 json!({
7358 "file1.txt": "content of file1",
7359 "file2.txt": "content of file2",
7360 "dir1": {},
7361 "dir2": {
7362 "file3.txt": "content of file3"
7363 }
7364 }),
7365 )
7366 .await;
7367
7368 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7369 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7370 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7371 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7372 cx.run_until_parked();
7373
7374 // Test 1: When only one file is selected, there should be no compare option
7375 select_path(&panel, "root/file1.txt", cx);
7376
7377 let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7378 assert_eq!(
7379 selected_files, None,
7380 "Should not have compare option when only one file is selected"
7381 );
7382
7383 // Test 2: When multiple files are selected, there should be a compare option
7384 select_path_with_mark(&panel, "root/file1.txt", cx);
7385 select_path_with_mark(&panel, "root/file2.txt", cx);
7386
7387 let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7388 assert!(
7389 selected_files.is_some(),
7390 "Should have files selected for comparison"
7391 );
7392 if let Some((file1, file2)) = selected_files {
7393 assert!(
7394 file1.to_string_lossy().ends_with("file1.txt")
7395 && file2.to_string_lossy().ends_with("file2.txt"),
7396 "Should have file1.txt and file2.txt as the selected files when multi-selecting"
7397 );
7398 }
7399
7400 // Test 3: Selecting a directory shouldn't count as a comparable file
7401 select_path_with_mark(&panel, "root/dir1", cx);
7402
7403 let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7404 assert!(
7405 selected_files.is_some(),
7406 "Directory selection should not affect comparable files"
7407 );
7408 if let Some((file1, file2)) = selected_files {
7409 assert!(
7410 file1.to_string_lossy().ends_with("file1.txt")
7411 && file2.to_string_lossy().ends_with("file2.txt"),
7412 "Selecting a directory should not affect the number of comparable files"
7413 );
7414 }
7415
7416 // Test 4: Selecting one more file
7417 select_path_with_mark(&panel, "root/dir2/file3.txt", cx);
7418
7419 let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
7420 assert!(
7421 selected_files.is_some(),
7422 "Directory selection should not affect comparable files"
7423 );
7424 if let Some((file1, file2)) = selected_files {
7425 assert!(
7426 file1.to_string_lossy().ends_with("file2.txt")
7427 && file2.to_string_lossy().ends_with("file3.txt"),
7428 "Selecting a directory should not affect the number of comparable files"
7429 );
7430 }
7431}
7432
7433#[gpui::test]
7434async fn test_hide_hidden_entries(cx: &mut gpui::TestAppContext) {
7435 init_test(cx);
7436
7437 let fs = FakeFs::new(cx.executor());
7438 fs.insert_tree(
7439 "/root",
7440 json!({
7441 ".hidden-file.txt": "hidden file content",
7442 "visible-file.txt": "visible file content",
7443 ".hidden-parent-dir": {
7444 "nested-dir": {
7445 "file.txt": "file content",
7446 }
7447 },
7448 "visible-dir": {
7449 "file-in-visible.txt": "file content",
7450 "nested": {
7451 ".hidden-nested-dir": {
7452 ".double-hidden-dir": {
7453 "deep-file-1.txt": "deep content 1",
7454 "deep-file-2.txt": "deep content 2"
7455 },
7456 "hidden-nested-file-1.txt": "hidden nested 1",
7457 "hidden-nested-file-2.txt": "hidden nested 2"
7458 },
7459 "visible-nested-file.txt": "visible nested content"
7460 }
7461 }
7462 }),
7463 )
7464 .await;
7465
7466 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7467 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7468 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7469
7470 cx.update(|_, cx| {
7471 let settings = *ProjectPanelSettings::get_global(cx);
7472 ProjectPanelSettings::override_global(
7473 ProjectPanelSettings {
7474 hide_hidden: false,
7475 ..settings
7476 },
7477 cx,
7478 );
7479 });
7480
7481 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7482 cx.run_until_parked();
7483
7484 toggle_expand_dir(&panel, "root/.hidden-parent-dir", cx);
7485 toggle_expand_dir(&panel, "root/.hidden-parent-dir/nested-dir", cx);
7486 toggle_expand_dir(&panel, "root/visible-dir", cx);
7487 toggle_expand_dir(&panel, "root/visible-dir/nested", cx);
7488 toggle_expand_dir(&panel, "root/visible-dir/nested/.hidden-nested-dir", cx);
7489 toggle_expand_dir(
7490 &panel,
7491 "root/visible-dir/nested/.hidden-nested-dir/.double-hidden-dir",
7492 cx,
7493 );
7494
7495 let expanded = [
7496 "v root",
7497 " v .hidden-parent-dir",
7498 " v nested-dir",
7499 " file.txt",
7500 " v visible-dir",
7501 " v nested",
7502 " v .hidden-nested-dir",
7503 " v .double-hidden-dir <== selected",
7504 " deep-file-1.txt",
7505 " deep-file-2.txt",
7506 " hidden-nested-file-1.txt",
7507 " hidden-nested-file-2.txt",
7508 " visible-nested-file.txt",
7509 " file-in-visible.txt",
7510 " .hidden-file.txt",
7511 " visible-file.txt",
7512 ];
7513
7514 assert_eq!(
7515 visible_entries_as_strings(&panel, 0..30, cx),
7516 &expanded,
7517 "With hide_hidden=false, contents of hidden nested directory should be visible"
7518 );
7519
7520 cx.update(|_, cx| {
7521 let settings = *ProjectPanelSettings::get_global(cx);
7522 ProjectPanelSettings::override_global(
7523 ProjectPanelSettings {
7524 hide_hidden: true,
7525 ..settings
7526 },
7527 cx,
7528 );
7529 });
7530
7531 panel.update_in(cx, |panel, window, cx| {
7532 panel.update_visible_entries(None, false, false, window, cx);
7533 });
7534 cx.run_until_parked();
7535
7536 assert_eq!(
7537 visible_entries_as_strings(&panel, 0..30, cx),
7538 &[
7539 "v root",
7540 " v visible-dir",
7541 " v nested",
7542 " visible-nested-file.txt",
7543 " file-in-visible.txt",
7544 " visible-file.txt",
7545 ],
7546 "With hide_hidden=false, contents of hidden nested directory should be visible"
7547 );
7548
7549 panel.update_in(cx, |panel, window, cx| {
7550 let settings = *ProjectPanelSettings::get_global(cx);
7551 ProjectPanelSettings::override_global(
7552 ProjectPanelSettings {
7553 hide_hidden: false,
7554 ..settings
7555 },
7556 cx,
7557 );
7558 panel.update_visible_entries(None, false, false, window, cx);
7559 });
7560 cx.run_until_parked();
7561
7562 assert_eq!(
7563 visible_entries_as_strings(&panel, 0..30, cx),
7564 &expanded,
7565 "With hide_hidden=false, deeply nested hidden directories and their contents should be visible"
7566 );
7567}
7568
7569fn select_path(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
7570 let path = rel_path(path);
7571 panel.update_in(cx, |panel, window, cx| {
7572 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
7573 let worktree = worktree.read(cx);
7574 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
7575 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
7576 panel.update_visible_entries(
7577 Some((worktree.id(), entry_id)),
7578 false,
7579 false,
7580 window,
7581 cx,
7582 );
7583 return;
7584 }
7585 }
7586 panic!("no worktree for path {:?}", path);
7587 });
7588 cx.run_until_parked();
7589}
7590
7591fn select_path_with_mark(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
7592 let path = rel_path(path);
7593 panel.update(cx, |panel, cx| {
7594 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
7595 let worktree = worktree.read(cx);
7596 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
7597 let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
7598 let entry = crate::SelectedEntry {
7599 worktree_id: worktree.id(),
7600 entry_id,
7601 };
7602 if !panel.marked_entries.contains(&entry) {
7603 panel.marked_entries.push(entry);
7604 }
7605 panel.state.selection = Some(entry);
7606 return;
7607 }
7608 }
7609 panic!("no worktree for path {:?}", path);
7610 });
7611}
7612
7613fn drag_selection_to(
7614 panel: &Entity<ProjectPanel>,
7615 target_path: &str,
7616 is_file: bool,
7617 cx: &mut VisualTestContext,
7618) {
7619 let target_entry = find_project_entry(panel, target_path, cx)
7620 .unwrap_or_else(|| panic!("no entry for target path {target_path:?}"));
7621
7622 panel.update_in(cx, |panel, window, cx| {
7623 let selection = panel
7624 .state
7625 .selection
7626 .expect("a selection is required before dragging");
7627 let drag = DraggedSelection {
7628 active_selection: SelectedEntry {
7629 worktree_id: selection.worktree_id,
7630 entry_id: panel.resolve_entry(selection.entry_id),
7631 },
7632 marked_selections: Arc::from(panel.marked_entries.clone()),
7633 };
7634 panel.drag_onto(&drag, target_entry, is_file, window, cx);
7635 });
7636 cx.executor().run_until_parked();
7637}
7638
7639fn find_project_entry(
7640 panel: &Entity<ProjectPanel>,
7641 path: &str,
7642 cx: &mut VisualTestContext,
7643) -> Option<ProjectEntryId> {
7644 let path = rel_path(path);
7645 panel.update(cx, |panel, cx| {
7646 for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
7647 let worktree = worktree.read(cx);
7648 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
7649 return worktree.entry_for_path(relative_path).map(|entry| entry.id);
7650 }
7651 }
7652 panic!("no worktree for path {path:?}");
7653 })
7654}
7655
7656fn visible_entries_as_strings(
7657 panel: &Entity<ProjectPanel>,
7658 range: Range<usize>,
7659 cx: &mut VisualTestContext,
7660) -> Vec<String> {
7661 let mut result = Vec::new();
7662 let mut project_entries = HashSet::default();
7663 let mut has_editor = false;
7664
7665 panel.update_in(cx, |panel, window, cx| {
7666 panel.for_each_visible_entry(range, window, cx, |project_entry, details, _, _| {
7667 if details.is_editing {
7668 assert!(!has_editor, "duplicate editor entry");
7669 has_editor = true;
7670 } else {
7671 assert!(
7672 project_entries.insert(project_entry),
7673 "duplicate project entry {:?} {:?}",
7674 project_entry,
7675 details
7676 );
7677 }
7678
7679 let indent = " ".repeat(details.depth);
7680 let icon = if details.kind.is_dir() {
7681 if details.is_expanded { "v " } else { "> " }
7682 } else {
7683 " "
7684 };
7685 #[cfg(windows)]
7686 let filename = details.filename.replace("\\", "/");
7687 #[cfg(not(windows))]
7688 let filename = details.filename;
7689 let name = if details.is_editing {
7690 format!("[EDITOR: '{}']", filename)
7691 } else if details.is_processing {
7692 format!("[PROCESSING: '{}']", filename)
7693 } else {
7694 filename
7695 };
7696 let selected = if details.is_selected {
7697 " <== selected"
7698 } else {
7699 ""
7700 };
7701 let marked = if details.is_marked {
7702 " <== marked"
7703 } else {
7704 ""
7705 };
7706
7707 result.push(format!("{indent}{icon}{name}{selected}{marked}"));
7708 });
7709 });
7710
7711 result
7712}
7713
7714/// Test that missing sort_mode field defaults to DirectoriesFirst
7715#[gpui::test]
7716async fn test_sort_mode_default_fallback(cx: &mut gpui::TestAppContext) {
7717 init_test(cx);
7718
7719 // Verify that when sort_mode is not specified, it defaults to DirectoriesFirst
7720 let default_settings = cx.read(|cx| *ProjectPanelSettings::get_global(cx));
7721 assert_eq!(
7722 default_settings.sort_mode,
7723 settings::ProjectPanelSortMode::DirectoriesFirst,
7724 "sort_mode should default to DirectoriesFirst"
7725 );
7726}
7727
7728/// Test sort modes: DirectoriesFirst (default) vs Mixed
7729#[gpui::test]
7730async fn test_sort_mode_directories_first(cx: &mut gpui::TestAppContext) {
7731 init_test(cx);
7732
7733 let fs = FakeFs::new(cx.executor());
7734 fs.insert_tree(
7735 "/root",
7736 json!({
7737 "zebra.txt": "",
7738 "Apple": {},
7739 "banana.rs": "",
7740 "Carrot": {},
7741 "aardvark.txt": "",
7742 }),
7743 )
7744 .await;
7745
7746 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7747 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7748 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7749 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7750 cx.run_until_parked();
7751
7752 // Default sort mode should be DirectoriesFirst
7753 assert_eq!(
7754 visible_entries_as_strings(&panel, 0..50, cx),
7755 &[
7756 "v root",
7757 " > Apple",
7758 " > Carrot",
7759 " aardvark.txt",
7760 " banana.rs",
7761 " zebra.txt",
7762 ]
7763 );
7764}
7765
7766#[gpui::test]
7767async fn test_sort_mode_mixed(cx: &mut gpui::TestAppContext) {
7768 init_test(cx);
7769
7770 let fs = FakeFs::new(cx.executor());
7771 fs.insert_tree(
7772 "/root",
7773 json!({
7774 "Zebra.txt": "",
7775 "apple": {},
7776 "Banana.rs": "",
7777 "carrot": {},
7778 "Aardvark.txt": "",
7779 }),
7780 )
7781 .await;
7782
7783 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7784 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7785 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7786
7787 // Switch to Mixed mode
7788 cx.update(|_, cx| {
7789 cx.update_global::<SettingsStore, _>(|store, cx| {
7790 store.update_user_settings(cx, |settings| {
7791 settings.project_panel.get_or_insert_default().sort_mode =
7792 Some(settings::ProjectPanelSortMode::Mixed);
7793 });
7794 });
7795 });
7796
7797 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7798 cx.run_until_parked();
7799
7800 // Mixed mode: case-insensitive sorting
7801 // Aardvark < apple < Banana < carrot < Zebra (all case-insensitive)
7802 assert_eq!(
7803 visible_entries_as_strings(&panel, 0..50, cx),
7804 &[
7805 "v root",
7806 " Aardvark.txt",
7807 " > apple",
7808 " Banana.rs",
7809 " > carrot",
7810 " Zebra.txt",
7811 ]
7812 );
7813}
7814
7815#[gpui::test]
7816async fn test_sort_mode_files_first(cx: &mut gpui::TestAppContext) {
7817 init_test(cx);
7818
7819 let fs = FakeFs::new(cx.executor());
7820 fs.insert_tree(
7821 "/root",
7822 json!({
7823 "Zebra.txt": "",
7824 "apple": {},
7825 "Banana.rs": "",
7826 "carrot": {},
7827 "Aardvark.txt": "",
7828 }),
7829 )
7830 .await;
7831
7832 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7833 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7834 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7835
7836 // Switch to FilesFirst mode
7837 cx.update(|_, cx| {
7838 cx.update_global::<SettingsStore, _>(|store, cx| {
7839 store.update_user_settings(cx, |settings| {
7840 settings.project_panel.get_or_insert_default().sort_mode =
7841 Some(settings::ProjectPanelSortMode::FilesFirst);
7842 });
7843 });
7844 });
7845
7846 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7847 cx.run_until_parked();
7848
7849 // FilesFirst mode: files first, then directories (both case-insensitive)
7850 assert_eq!(
7851 visible_entries_as_strings(&panel, 0..50, cx),
7852 &[
7853 "v root",
7854 " Aardvark.txt",
7855 " Banana.rs",
7856 " Zebra.txt",
7857 " > apple",
7858 " > carrot",
7859 ]
7860 );
7861}
7862
7863#[gpui::test]
7864async fn test_sort_mode_toggle(cx: &mut gpui::TestAppContext) {
7865 init_test(cx);
7866
7867 let fs = FakeFs::new(cx.executor());
7868 fs.insert_tree(
7869 "/root",
7870 json!({
7871 "file2.txt": "",
7872 "dir1": {},
7873 "file1.txt": "",
7874 }),
7875 )
7876 .await;
7877
7878 let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
7879 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7880 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7881 let panel = workspace.update(cx, ProjectPanel::new).unwrap();
7882 cx.run_until_parked();
7883
7884 // Initially DirectoriesFirst
7885 assert_eq!(
7886 visible_entries_as_strings(&panel, 0..50, cx),
7887 &["v root", " > dir1", " file1.txt", " file2.txt",]
7888 );
7889
7890 // Toggle to Mixed
7891 cx.update(|_, cx| {
7892 cx.update_global::<SettingsStore, _>(|store, cx| {
7893 store.update_user_settings(cx, |settings| {
7894 settings.project_panel.get_or_insert_default().sort_mode =
7895 Some(settings::ProjectPanelSortMode::Mixed);
7896 });
7897 });
7898 });
7899 cx.run_until_parked();
7900
7901 assert_eq!(
7902 visible_entries_as_strings(&panel, 0..50, cx),
7903 &["v root", " > dir1", " file1.txt", " file2.txt",]
7904 );
7905
7906 // Toggle back to DirectoriesFirst
7907 cx.update(|_, cx| {
7908 cx.update_global::<SettingsStore, _>(|store, cx| {
7909 store.update_user_settings(cx, |settings| {
7910 settings.project_panel.get_or_insert_default().sort_mode =
7911 Some(settings::ProjectPanelSortMode::DirectoriesFirst);
7912 });
7913 });
7914 });
7915 cx.run_until_parked();
7916
7917 assert_eq!(
7918 visible_entries_as_strings(&panel, 0..50, cx),
7919 &["v root", " > dir1", " file1.txt", " file2.txt",]
7920 );
7921}
7922
7923fn init_test(cx: &mut TestAppContext) {
7924 cx.update(|cx| {
7925 let settings_store = SettingsStore::test(cx);
7926 cx.set_global(settings_store);
7927 theme::init(theme::LoadThemes::JustBase, cx);
7928 crate::init(cx);
7929
7930 cx.update_global::<SettingsStore, _>(|store, cx| {
7931 store.update_user_settings(cx, |settings| {
7932 settings
7933 .project_panel
7934 .get_or_insert_default()
7935 .auto_fold_dirs = Some(false);
7936 settings.project.worktree.file_scan_exclusions = Some(Vec::new());
7937 });
7938 });
7939 });
7940}
7941
7942fn init_test_with_editor(cx: &mut TestAppContext) {
7943 cx.update(|cx| {
7944 let app_state = AppState::test(cx);
7945 theme::init(theme::LoadThemes::JustBase, cx);
7946 editor::init(cx);
7947 crate::init(cx);
7948 workspace::init(app_state, cx);
7949
7950 cx.update_global::<SettingsStore, _>(|store, cx| {
7951 store.update_user_settings(cx, |settings| {
7952 settings
7953 .project_panel
7954 .get_or_insert_default()
7955 .auto_fold_dirs = Some(false);
7956 settings.project.worktree.file_scan_exclusions = Some(Vec::new())
7957 });
7958 });
7959 });
7960}
7961
7962fn set_auto_open_settings(
7963 cx: &mut TestAppContext,
7964 auto_open_settings: ProjectPanelAutoOpenSettings,
7965) {
7966 cx.update(|cx| {
7967 cx.update_global::<SettingsStore, _>(|store, cx| {
7968 store.update_user_settings(cx, |settings| {
7969 settings.project_panel.get_or_insert_default().auto_open = Some(auto_open_settings);
7970 });
7971 })
7972 });
7973}
7974
7975fn ensure_single_file_is_opened(
7976 window: &WindowHandle<Workspace>,
7977 expected_path: &str,
7978 cx: &mut TestAppContext,
7979) {
7980 window
7981 .update(cx, |workspace, _, cx| {
7982 let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
7983 assert_eq!(worktrees.len(), 1);
7984 let worktree_id = worktrees[0].read(cx).id();
7985
7986 let open_project_paths = workspace
7987 .panes()
7988 .iter()
7989 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
7990 .collect::<Vec<_>>();
7991 assert_eq!(
7992 open_project_paths,
7993 vec![ProjectPath {
7994 worktree_id,
7995 path: Arc::from(rel_path(expected_path))
7996 }],
7997 "Should have opened file, selected in project panel"
7998 );
7999 })
8000 .unwrap();
8001}
8002
8003fn submit_deletion(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
8004 assert!(
8005 !cx.has_pending_prompt(),
8006 "Should have no prompts before the deletion"
8007 );
8008 panel.update_in(cx, |panel, window, cx| {
8009 panel.delete(&Delete { skip_prompt: false }, window, cx)
8010 });
8011 assert!(
8012 cx.has_pending_prompt(),
8013 "Should have a prompt after the deletion"
8014 );
8015 cx.simulate_prompt_answer("Delete");
8016 assert!(
8017 !cx.has_pending_prompt(),
8018 "Should have no prompts after prompt was replied to"
8019 );
8020 cx.executor().run_until_parked();
8021}
8022
8023fn submit_deletion_skipping_prompt(panel: &Entity<ProjectPanel>, cx: &mut VisualTestContext) {
8024 assert!(
8025 !cx.has_pending_prompt(),
8026 "Should have no prompts before the deletion"
8027 );
8028 panel.update_in(cx, |panel, window, cx| {
8029 panel.delete(&Delete { skip_prompt: true }, window, cx)
8030 });
8031 assert!(!cx.has_pending_prompt(), "Should have received no prompts");
8032 cx.executor().run_until_parked();
8033}
8034
8035fn ensure_no_open_items_and_panes(workspace: &WindowHandle<Workspace>, cx: &mut VisualTestContext) {
8036 assert!(
8037 !cx.has_pending_prompt(),
8038 "Should have no prompts after deletion operation closes the file"
8039 );
8040 workspace
8041 .read_with(cx, |workspace, cx| {
8042 let open_project_paths = workspace
8043 .panes()
8044 .iter()
8045 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
8046 .collect::<Vec<_>>();
8047 assert!(
8048 open_project_paths.is_empty(),
8049 "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
8050 );
8051 })
8052 .unwrap();
8053}
8054
8055struct TestProjectItemView {
8056 focus_handle: FocusHandle,
8057 path: ProjectPath,
8058}
8059
8060struct TestProjectItem {
8061 path: ProjectPath,
8062}
8063
8064impl project::ProjectItem for TestProjectItem {
8065 fn try_open(
8066 _project: &Entity<Project>,
8067 path: &ProjectPath,
8068 cx: &mut App,
8069 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
8070 let path = path.clone();
8071 Some(cx.spawn(async move |cx| cx.new(|_| Self { path })))
8072 }
8073
8074 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8075 None
8076 }
8077
8078 fn project_path(&self, _: &App) -> Option<ProjectPath> {
8079 Some(self.path.clone())
8080 }
8081
8082 fn is_dirty(&self) -> bool {
8083 false
8084 }
8085}
8086
8087impl ProjectItem for TestProjectItemView {
8088 type Item = TestProjectItem;
8089
8090 fn for_project_item(
8091 _: Entity<Project>,
8092 _: Option<&Pane>,
8093 project_item: Entity<Self::Item>,
8094 _: &mut Window,
8095 cx: &mut Context<Self>,
8096 ) -> Self
8097 where
8098 Self: Sized,
8099 {
8100 Self {
8101 path: project_item.update(cx, |project_item, _| project_item.path.clone()),
8102 focus_handle: cx.focus_handle(),
8103 }
8104 }
8105}
8106
8107impl Item for TestProjectItemView {
8108 type Event = ();
8109
8110 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
8111 "Test".into()
8112 }
8113}
8114
8115impl EventEmitter<()> for TestProjectItemView {}
8116
8117impl Focusable for TestProjectItemView {
8118 fn focus_handle(&self, _: &App) -> FocusHandle {
8119 self.focus_handle.clone()
8120 }
8121}
8122
8123impl Render for TestProjectItemView {
8124 fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
8125 Empty
8126 }
8127}