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