1mod component;
4mod events;
5mod explorer;
6mod fs;
7mod git;
8mod mod_private;
9
10pub use crate::style::FileIconPalette;
11pub use events::{FileTreeEvent, FileTreeToggleEvent};
12pub use fs::{FileIconStyle, FileKind};
13pub use git::{GitChangeState, GitFileStatus, GitIconStyle};
14pub(crate) use mod_private::FileTreeProps;
15use mod_private::{
16 default_git_style_added, default_git_style_conflicted, default_git_style_deleted,
17 default_git_style_modified, default_git_style_renamed, default_git_style_untracked,
18};
19
20use crate::callback::Callback;
21use crate::core::element::Element;
22use crate::style::{BorderStyle, Color, Length, Padding, ScrollbarConfig, Style, StyleSlot};
23use crate::widgets::{ScrollKeymap, TreeKeymap};
24use std::collections::{HashMap, HashSet};
25use std::sync::Arc;
26
27pub use crate::utils::file_icons::FileIconOverride;
28
29#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub struct FileTreeItemStyle {
32 pub row: Option<Style>,
34 pub icon: Option<Style>,
36 pub label: Option<Style>,
38 pub suffix: Option<Style>,
40}
41
42impl FileTreeItemStyle {
43 pub fn new() -> Self {
45 Self::default()
46 }
47
48 pub fn row(mut self, style: Style) -> Self {
50 self.row = Some(style);
51 self
52 }
53
54 pub fn icon(mut self, style: Style) -> Self {
56 self.icon = Some(style);
57 self
58 }
59
60 pub fn label(mut self, style: Style) -> Self {
62 self.label = Some(style);
63 self
64 }
65
66 pub fn suffix(mut self, style: Style) -> Self {
68 self.suffix = Some(style);
69 self
70 }
71}
72
73#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
75pub enum FileTreeSuffixPriority {
76 #[default]
78 Label,
79 Suffix,
81}
82
83#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
85pub enum FileTreeChangeView {
86 #[default]
88 AllFiles,
89 ChangedOnly,
91}
92
93pub type FileTreeGitView = FileTreeChangeView;
95
96#[derive(Clone, Debug, Default, PartialEq, Eq)]
98pub enum FileTreeChangeSource {
99 #[default]
101 Git,
102 Provided(Vec<FileTreeChange>),
104}
105
106impl FileTreeChangeSource {
107 pub fn provided(changes: impl IntoIterator<Item = FileTreeChange>) -> Self {
109 Self::Provided(changes.into_iter().collect())
110 }
111}
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
115pub enum FileTreeChangeStatus {
116 Modified,
118 Added,
120 Deleted,
122 Renamed,
124 Untracked,
126 Conflicted,
128}
129
130impl From<FileTreeChangeStatus> for GitChangeState {
131 fn from(status: FileTreeChangeStatus) -> Self {
132 match status {
133 FileTreeChangeStatus::Modified => Self::Modified,
134 FileTreeChangeStatus::Added => Self::Added,
135 FileTreeChangeStatus::Deleted => Self::Deleted,
136 FileTreeChangeStatus::Renamed => Self::Renamed,
137 FileTreeChangeStatus::Untracked => Self::Untracked,
138 FileTreeChangeStatus::Conflicted => Self::Conflicted,
139 }
140 }
141}
142
143#[derive(Clone, Debug, PartialEq, Eq)]
145pub struct FileTreeChange {
146 pub path: Arc<str>,
148 pub status: FileTreeChangeStatus,
150 pub kind: Option<FileKind>,
152 pub additions: usize,
154 pub deletions: usize,
156 pub staged: bool,
158}
159
160impl FileTreeChange {
161 pub fn new(path: impl Into<Arc<str>>, status: FileTreeChangeStatus) -> Self {
163 Self {
164 path: path.into(),
165 status,
166 kind: None,
167 additions: 0,
168 deletions: 0,
169 staged: false,
170 }
171 }
172
173 pub fn kind(mut self, kind: FileKind) -> Self {
175 self.kind = Some(kind);
176 self
177 }
178
179 pub fn diff_stat(mut self, additions: usize, deletions: usize) -> Self {
181 self.additions = additions;
182 self.deletions = deletions;
183 self
184 }
185
186 pub fn additions(mut self, additions: usize) -> Self {
188 self.additions = additions;
189 self
190 }
191
192 pub fn deletions(mut self, deletions: usize) -> Self {
194 self.deletions = deletions;
195 self
196 }
197
198 pub fn staged(mut self, staged: bool) -> Self {
200 self.staged = staged;
201 self
202 }
203}
204
205#[derive(Clone)]
207pub struct FileTree {
208 props: FileTreeProps,
209}
210
211impl FileTree {
212 pub fn new(root: impl Into<Arc<str>>) -> Self {
214 Self {
215 props: FileTreeProps {
216 root: root.into(),
217 show_hidden: false,
218 max_entries_per_dir: 2_000,
219 show_icons: true,
220 icon_style: FileIconStyle::default(),
221 icon_palette: FileIconPalette::default(),
222 icon_overrides: HashMap::new(),
223 show_arrows: true,
224 indent_style: crate::widgets::IndentStyle::None,
225 indent_guide_style: Style::default(),
226 directory_icon: "[D]".into(),
227 opened_directory_icon: "[D]".into(),
228 file_icon: "[F]".into(),
229 symlink_icon: "[L]".into(),
230 other_icon: "[?]".into(),
231 directory_label_style: Style::default(),
232 file_label_style: Style::default(),
233 loading_label: "loading...".into(),
234 error_prefix: "error:".into(),
235 width: Length::Flex(1),
236 height: Length::Flex(1),
237 style: Style::default(),
238 hover_style: StyleSlot::Inherit,
239 item_hover_style: StyleSlot::Inherit,
240 selection_style: StyleSlot::Inherit,
241 unfocused_selection_style: StyleSlot::Inherit,
242 selected: None,
243 selected_path: None,
244 reveal_path: None,
245 select_path: None,
246 force_scroll_to_selected: false,
247 expanded_paths: None,
248 selection_symbol: None,
249 selection_symbol_style: None,
250 unfocused_selection_symbol_style: None,
251 scrollbar: true,
252 scrollbar_config: ScrollbarConfig::default(),
253 scroll_keys: ScrollKeymap::default(),
254 scroll_wheel: true,
255 show_scroll_indicators: false,
256 scroll_indicator_style: Style::default(),
257 empty_text: Some("Directory is empty".into()),
258 empty_text_style: Style::default(),
259 explorer: false,
260 explorer_placeholder: "Find files...".into(),
261 explorer_prefix: " ".into(),
262 explorer_input_border: false,
263 explorer_input_border_style: BorderStyle::Plain,
264 explorer_input_padding: Padding {
265 left: 1,
266 right: 0,
267 top: 0,
268 bottom: 0,
269 },
270 explorer_input_style: Style::default(),
271 explorer_input_focus_style: StyleSlot::Inherit,
272 explorer_input_focus_content_style: Style::default(),
273 explorer_placeholder_style: Style::default(),
274 explorer_focus_placeholder_style: Style::default(),
275 explorer_match_style: Style::default(),
276 explorer_divider: true,
277 explorer_divider_join_frame: true,
278 explorer_divider_char: '─',
279 explorer_divider_style: Style::default(),
280 activate_on_click: true,
281 focusable: true,
282 keymap: TreeKeymap::default(),
283 git_status: true,
284 highlight_changed_labels: false,
285 change_source: FileTreeChangeSource::default(),
286 change_view: FileTreeChangeView::default(),
287 git_diff_stats: false,
288 git_icon_style: GitIconStyle::NerdFont,
289 git_refresh_nonce: 0,
290 git_marker_modified: "M".into(),
291 git_marker_added: "A".into(),
292 git_marker_deleted: "D".into(),
293 git_marker_renamed: "R".into(),
294 git_marker_untracked: "?".into(),
295 git_marker_conflicted: "!".into(),
296 git_style_modified: default_git_style_modified(),
299 git_style_added: default_git_style_added(),
300 git_style_deleted: default_git_style_deleted(),
301 git_style_renamed: default_git_style_renamed(),
302 git_style_untracked: default_git_style_untracked(),
303 git_style_conflicted: default_git_style_conflicted(),
304 change_suffix_style: Style::default(),
305 change_suffix_priority: FileTreeSuffixPriority::default(),
306 path_styles: HashMap::new(),
307 on_select: None,
308 on_activate: None,
309 on_toggle: None,
310 },
311 }
312 }
313
314 pub fn show_hidden(mut self, show_hidden: bool) -> Self {
316 self.props.show_hidden = show_hidden;
317 self
318 }
319
320 pub fn max_entries_per_dir(mut self, max_entries: usize) -> Self {
322 self.props.max_entries_per_dir = max_entries.max(1);
323 self
324 }
325
326 pub fn show_icons(mut self, show_icons: bool) -> Self {
328 self.props.show_icons = show_icons;
329 self
330 }
331
332 pub fn icon_style(mut self, style: FileIconStyle) -> Self {
334 self.props.icon_style = style;
335 self
336 }
337
338 pub fn icon_palette(mut self, palette: FileIconPalette) -> Self {
340 self.props.icon_palette = palette;
341 self
342 }
343
344 pub fn icon_override(
349 mut self,
350 pattern: impl Into<Arc<str>>,
351 icon: impl Into<Arc<str>>,
352 color: Option<Color>,
353 ) -> Self {
354 self.props.icon_overrides.insert(
355 pattern.into(),
356 FileIconOverride {
357 icon: icon.into(),
358 color,
359 },
360 );
361 self
362 }
363
364 pub fn show_arrows(mut self, show: bool) -> Self {
366 self.props.show_arrows = show;
367 self
368 }
369
370 pub fn indent_style(mut self, style: crate::widgets::IndentStyle) -> Self {
372 self.props.indent_style = style;
373 self
374 }
375
376 pub fn indent_guide_style(mut self, style: Style) -> Self {
378 self.props.indent_guide_style = style;
379 self
380 }
381
382 pub fn directory_icon(mut self, icon: impl Into<Arc<str>>) -> Self {
384 self.props.directory_icon = icon.into();
385 self
386 }
387
388 pub fn opened_directory_icon(mut self, icon: impl Into<Arc<str>>) -> Self {
390 self.props.opened_directory_icon = icon.into();
391 self
392 }
393
394 pub fn file_icon(mut self, icon: impl Into<Arc<str>>) -> Self {
396 self.props.file_icon = icon.into();
397 self
398 }
399
400 pub fn symlink_icon(mut self, icon: impl Into<Arc<str>>) -> Self {
402 self.props.symlink_icon = icon.into();
403 self
404 }
405
406 pub fn other_icon(mut self, icon: impl Into<Arc<str>>) -> Self {
408 self.props.other_icon = icon.into();
409 self
410 }
411
412 pub fn directory_label_style(mut self, style: Style) -> Self {
414 self.props.directory_label_style = style;
415 self
416 }
417
418 pub fn file_label_style(mut self, style: Style) -> Self {
420 self.props.file_label_style = style;
421 self
422 }
423
424 pub fn path_style(mut self, path: impl Into<Arc<str>>, style: FileTreeItemStyle) -> Self {
426 self.props.path_styles.insert(path.into(), style);
427 self
428 }
429
430 pub fn path_styles(
432 mut self,
433 styles: impl IntoIterator<Item = (impl Into<Arc<str>>, FileTreeItemStyle)>,
434 ) -> Self {
435 self.props
436 .path_styles
437 .extend(styles.into_iter().map(|(path, style)| (path.into(), style)));
438 self
439 }
440
441 pub fn loading_label(mut self, label: impl Into<Arc<str>>) -> Self {
443 self.props.loading_label = label.into();
444 self
445 }
446
447 pub fn error_prefix(mut self, prefix: impl Into<Arc<str>>) -> Self {
449 self.props.error_prefix = prefix.into();
450 self
451 }
452
453 pub fn width(mut self, width: Length) -> Self {
455 self.props.width = width;
456 self
457 }
458
459 pub fn height(mut self, height: Length) -> Self {
461 self.props.height = height;
462 self
463 }
464
465 pub fn style(mut self, style: Style) -> Self {
467 self.props.style = style;
468 self
469 }
470
471 pub fn hover_style(mut self, style: Style) -> Self {
473 self.props.hover_style = StyleSlot::Replace(style);
474 self
475 }
476
477 pub fn extend_hover_style(mut self, style: Style) -> Self {
479 self.props.hover_style = StyleSlot::Extend(style);
480 self
481 }
482
483 pub fn inherit_hover_style(mut self) -> Self {
485 self.props.hover_style = StyleSlot::Inherit;
486 self
487 }
488
489 pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
491 self.props.hover_style = slot;
492 self
493 }
494
495 pub fn item_hover_style(mut self, style: Style) -> Self {
497 self.props.item_hover_style = StyleSlot::Replace(style);
498 self
499 }
500
501 pub fn extend_item_hover_style(mut self, style: Style) -> Self {
503 self.props.item_hover_style = StyleSlot::Extend(style);
504 self
505 }
506
507 pub fn inherit_item_hover_style(mut self) -> Self {
509 self.props.item_hover_style = StyleSlot::Inherit;
510 self
511 }
512
513 pub fn item_hover_style_slot(mut self, slot: StyleSlot) -> Self {
515 self.props.item_hover_style = slot;
516 self
517 }
518
519 pub fn selection_style(mut self, style: Style) -> Self {
521 self.props.selection_style = StyleSlot::Replace(style);
522 self
523 }
524
525 pub fn extend_selection_style(mut self, style: Style) -> Self {
527 self.props.selection_style = StyleSlot::Extend(style);
528 self
529 }
530
531 pub fn inherit_selection_style(mut self) -> Self {
533 self.props.selection_style = StyleSlot::Inherit;
534 self
535 }
536
537 pub fn selection_style_slot(mut self, slot: StyleSlot) -> Self {
539 self.props.selection_style = slot;
540 self
541 }
542
543 pub fn unfocused_selection_style(mut self, style: Style) -> Self {
545 self.props.unfocused_selection_style = StyleSlot::Replace(style);
546 self
547 }
548
549 pub fn extend_unfocused_selection_style(mut self, style: Style) -> Self {
551 self.props.unfocused_selection_style = StyleSlot::Extend(style);
552 self
553 }
554
555 pub fn inherit_unfocused_selection_style(mut self) -> Self {
557 self.props.unfocused_selection_style = StyleSlot::Inherit;
558 self
559 }
560
561 pub fn unfocused_selection_style_slot(mut self, slot: StyleSlot) -> Self {
563 self.props.unfocused_selection_style = slot;
564 self
565 }
566
567 pub fn selected(mut self, selected: usize) -> Self {
569 self.props.selected = Some(selected);
570 self
571 }
572
573 pub fn selected_path(mut self, path: impl Into<Arc<str>>) -> Self {
578 self.props.selected_path = Some(path.into());
579 self
580 }
581
582 pub fn reveal_path(mut self, path: impl Into<Arc<str>>) -> Self {
588 self.props.reveal_path = Some(path.into());
589 self
590 }
591
592 pub fn select_path(mut self, path: impl Into<Arc<str>>) -> Self {
598 self.props.select_path = Some(path.into());
599 self
600 }
601
602 pub fn force_scroll_to_selected(mut self, force: bool) -> Self {
604 self.props.force_scroll_to_selected = force;
605 self
606 }
607
608 pub fn expanded_paths(mut self, paths: impl IntoIterator<Item = impl Into<Arc<str>>>) -> Self {
610 self.props.expanded_paths = Some(paths.into_iter().map(Into::into).collect::<HashSet<_>>());
611 self
612 }
613
614 pub fn selection_symbol(mut self, symbol: Option<impl Into<Arc<str>>>) -> Self {
616 self.props.selection_symbol = symbol.map(Into::into);
617 self
618 }
619
620 pub fn selection_symbol_style(mut self, style: Option<Style>) -> Self {
622 self.props.selection_symbol_style = style;
623 self
624 }
625
626 pub fn unfocused_selection_symbol_style(mut self, style: Option<Style>) -> Self {
628 self.props.unfocused_selection_symbol_style = style;
629 self
630 }
631
632 pub fn scrollbar(mut self, scrollbar: bool) -> Self {
634 self.props.scrollbar = scrollbar;
635 self
636 }
637
638 pub fn scrollbar_config(mut self, config: ScrollbarConfig) -> Self {
640 self.props.scrollbar_config = config;
641 self
642 }
643
644 pub fn scroll_keys(mut self, keys: ScrollKeymap) -> Self {
646 self.props.scroll_keys = keys;
647 self
648 }
649
650 pub fn scroll_wheel(mut self, enabled: bool) -> Self {
652 self.props.scroll_wheel = enabled;
653 self
654 }
655
656 pub fn show_scroll_indicators(mut self, show: bool) -> Self {
658 self.props.show_scroll_indicators = show;
659 self
660 }
661
662 pub fn scroll_indicator_style(mut self, style: Style) -> Self {
664 self.props.scroll_indicator_style = style;
665 self
666 }
667
668 pub fn empty_text(mut self, text: impl Into<Arc<str>>) -> Self {
670 self.props.empty_text = Some(text.into());
671 self
672 }
673
674 pub fn empty_text_style(mut self, style: Style) -> Self {
676 self.props.empty_text_style = style;
677 self
678 }
679
680 pub fn explorer(mut self, explorer: bool) -> Self {
682 self.props.explorer = explorer;
683 self
684 }
685
686 pub fn explorer_placeholder(mut self, text: impl Into<Arc<str>>) -> Self {
688 self.props.explorer_placeholder = text.into();
689 self
690 }
691
692 pub fn explorer_prefix(mut self, prefix: impl Into<Arc<str>>) -> Self {
694 self.props.explorer_prefix = prefix.into();
695 self
696 }
697
698 pub fn explorer_input_border(mut self, border: bool) -> Self {
700 self.props.explorer_input_border = border;
701 self
702 }
703
704 pub fn explorer_input_border_style(mut self, style: BorderStyle) -> Self {
706 self.props.explorer_input_border_style = style;
707 self
708 }
709
710 pub fn explorer_input_padding(mut self, padding: impl Into<Padding>) -> Self {
712 self.props.explorer_input_padding = padding.into();
713 self
714 }
715
716 pub fn explorer_input_style(mut self, style: Style) -> Self {
718 self.props.explorer_input_style = style;
719 self
720 }
721
722 pub fn explorer_input_focus_style(mut self, style: Style) -> Self {
724 self.props.explorer_input_focus_style = StyleSlot::Replace(style);
725 self
726 }
727
728 pub fn extend_explorer_input_focus_style(mut self, style: Style) -> Self {
730 self.props.explorer_input_focus_style = StyleSlot::Extend(style);
731 self
732 }
733
734 pub fn inherit_explorer_input_focus_style(mut self) -> Self {
736 self.props.explorer_input_focus_style = StyleSlot::Inherit;
737 self
738 }
739
740 pub fn explorer_input_focus_style_slot(mut self, slot: StyleSlot) -> Self {
742 self.props.explorer_input_focus_style = slot;
743 self
744 }
745
746 pub fn explorer_input_focus_content_style(mut self, style: Style) -> Self {
748 self.props.explorer_input_focus_content_style = style;
749 self
750 }
751
752 pub fn explorer_placeholder_style(mut self, style: Style) -> Self {
754 self.props.explorer_placeholder_style = style;
755 self
756 }
757
758 pub fn explorer_focus_placeholder_style(mut self, style: Style) -> Self {
760 self.props.explorer_focus_placeholder_style = style;
761 self
762 }
763
764 pub fn explorer_match_style(mut self, style: Style) -> Self {
766 self.props.explorer_match_style = style;
767 self
768 }
769
770 pub fn explorer_divider(mut self, show: bool) -> Self {
772 self.props.explorer_divider = show;
773 self
774 }
775
776 pub fn explorer_divider_join_frame(mut self, join: bool) -> Self {
778 self.props.explorer_divider_join_frame = join;
779 self
780 }
781
782 pub fn explorer_divider_char(mut self, ch: char) -> Self {
784 self.props.explorer_divider_char = ch;
785 self
786 }
787
788 pub fn explorer_divider_style(mut self, style: Style) -> Self {
790 self.props.explorer_divider_style = style;
791 self
792 }
793
794 pub fn activate_on_click(mut self, activate_on_click: bool) -> Self {
796 self.props.activate_on_click = activate_on_click;
797 self
798 }
799
800 pub fn focusable(mut self, focusable: bool) -> Self {
802 self.props.focusable = focusable;
803 self
804 }
805
806 pub fn keymap(mut self, keymap: TreeKeymap) -> Self {
808 self.props.keymap = keymap;
809 self
810 }
811
812 pub fn git_status(mut self, enabled: bool) -> Self {
814 self.props.git_status = enabled;
815 self
816 }
817
818 pub fn highlight_changed_labels(mut self, enabled: bool) -> Self {
824 self.props.highlight_changed_labels = enabled;
825 self
826 }
827
828 pub fn git_view(mut self, view: FileTreeGitView) -> Self {
830 self.props.change_view = view;
831 self
832 }
833
834 pub fn change_source(mut self, source: FileTreeChangeSource) -> Self {
836 self.props.change_source = source;
837 self
838 }
839
840 pub fn change_view(mut self, view: FileTreeChangeView) -> Self {
842 self.props.change_view = view;
843 self
844 }
845
846 pub fn git_changed_only(mut self, enabled: bool) -> Self {
848 self.props.change_view = if enabled {
849 FileTreeChangeView::ChangedOnly
850 } else {
851 FileTreeChangeView::AllFiles
852 };
853 self
854 }
855
856 pub fn show_diff_stats(self, enabled: bool) -> Self {
858 self.git_diff_stats(enabled)
859 }
860
861 pub fn git_diff_stats(mut self, enabled: bool) -> Self {
863 self.props.git_diff_stats = enabled;
864 self
865 }
866
867 pub fn git_icon_style(mut self, style: GitIconStyle) -> Self {
869 self.props.git_icon_style = style;
870 self
871 }
872
873 pub fn refresh_git_status(mut self) -> Self {
877 use std::sync::atomic::{AtomicU64, Ordering};
878 static FILE_TREE_REFRESH_NONCE: AtomicU64 = AtomicU64::new(1);
879 self.props.git_refresh_nonce = FILE_TREE_REFRESH_NONCE.fetch_add(1, Ordering::Relaxed);
880 self
881 }
882
883 pub fn git_refresh_token(mut self, token: u64) -> Self {
887 self.props.git_refresh_nonce = token;
888 self
889 }
890
891 pub fn git_marker_modified(mut self, marker: impl Into<Arc<str>>) -> Self {
893 self.props.git_marker_modified = marker.into();
894 self
895 }
896
897 pub fn git_marker_added(mut self, marker: impl Into<Arc<str>>) -> Self {
899 self.props.git_marker_added = marker.into();
900 self
901 }
902
903 pub fn git_marker_deleted(mut self, marker: impl Into<Arc<str>>) -> Self {
905 self.props.git_marker_deleted = marker.into();
906 self
907 }
908
909 pub fn git_marker_renamed(mut self, marker: impl Into<Arc<str>>) -> Self {
911 self.props.git_marker_renamed = marker.into();
912 self
913 }
914
915 pub fn git_marker_untracked(mut self, marker: impl Into<Arc<str>>) -> Self {
917 self.props.git_marker_untracked = marker.into();
918 self
919 }
920
921 pub fn git_marker_conflicted(mut self, marker: impl Into<Arc<str>>) -> Self {
923 self.props.git_marker_conflicted = marker.into();
924 self
925 }
926
927 pub fn git_style_modified(mut self, style: Style) -> Self {
929 self.props.git_style_modified = style;
930 self
931 }
932
933 pub fn git_style_added(mut self, style: Style) -> Self {
935 self.props.git_style_added = style;
936 self
937 }
938
939 pub fn git_style_deleted(mut self, style: Style) -> Self {
941 self.props.git_style_deleted = style;
942 self
943 }
944
945 pub fn git_style_renamed(mut self, style: Style) -> Self {
947 self.props.git_style_renamed = style;
948 self
949 }
950
951 pub fn git_style_untracked(mut self, style: Style) -> Self {
953 self.props.git_style_untracked = style;
954 self
955 }
956
957 pub fn git_style_conflicted(mut self, style: Style) -> Self {
959 self.props.git_style_conflicted = style;
960 self
961 }
962
963 pub fn change_suffix_style(mut self, style: Style) -> Self {
965 self.props.change_suffix_style = style;
966 self
967 }
968
969 pub fn git_suffix_style(self, style: Style) -> Self {
971 self.change_suffix_style(style)
972 }
973
974 pub fn change_suffix_priority(mut self, priority: FileTreeSuffixPriority) -> Self {
976 self.props.change_suffix_priority = priority;
977 self
978 }
979
980 pub fn git_suffix_priority(self, priority: FileTreeSuffixPriority) -> Self {
982 self.change_suffix_priority(priority)
983 }
984
985 pub fn on_select(mut self, cb: Callback<FileTreeEvent>) -> Self {
987 self.props.on_select = Some(cb);
988 self
989 }
990
991 pub fn on_activate(mut self, cb: Callback<FileTreeEvent>) -> Self {
993 self.props.on_activate = Some(cb);
994 self
995 }
996
997 pub fn on_toggle(mut self, cb: Callback<FileTreeToggleEvent>) -> Self {
999 self.props.on_toggle = Some(cb);
1000 self
1001 }
1002}
1003
1004impl From<FileTree> for Element {
1005 fn from(file_tree: FileTree) -> Self {
1006 let root_key = file_tree.props.root.clone();
1007 crate::child(component::FileTreeComponent::new, file_tree.props).key(root_key)
1008 }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use super::*;
1014 use std::collections::{HashMap, HashSet};
1015 use std::sync::Arc;
1016
1017 #[test]
1018 fn parses_untracked_line() {
1019 use git::GitChangeState;
1020 use git::parse_git_porcelain_line;
1021 let parsed = parse_git_porcelain_line("?? src/new.rs");
1022 assert_eq!(
1023 parsed.map(|(p, s)| (p, s.unstaged)),
1024 Some(("src/new.rs", Some(GitChangeState::Untracked)))
1025 );
1026 }
1027
1028 #[test]
1029 fn parses_rename_destination() {
1030 use git::GitChangeState;
1031 use git::parse_git_porcelain_line;
1032 let parsed = parse_git_porcelain_line("R old/name.rs -> new/name.rs");
1033 assert_eq!(
1034 parsed.map(|(p, s)| (p, s.staged)),
1035 Some(("new/name.rs", Some(GitChangeState::Renamed)))
1036 );
1037 }
1038
1039 #[test]
1040 fn conflicting_status_wins_priority() {
1041 use git::insert_status;
1042 use git::{GitChangeState, GitFileStatus};
1043 let mut statuses = HashMap::new();
1044 let key: Arc<str> = "/repo/src/app.rs".into();
1045
1046 insert_status(
1047 &mut statuses,
1048 key.clone(),
1049 GitFileStatus::new(None, Some(GitChangeState::Modified)),
1050 );
1051 insert_status(
1052 &mut statuses,
1053 key.clone(),
1054 GitFileStatus::new(None, Some(GitChangeState::Conflicted)),
1055 );
1056 insert_status(
1057 &mut statuses,
1058 key.clone(),
1059 GitFileStatus::new(None, Some(GitChangeState::Added)),
1060 );
1061
1062 assert_eq!(
1063 statuses.get(key.as_ref()).copied().and_then(|s| s.unstaged),
1064 Some(GitChangeState::Conflicted)
1065 );
1066 }
1067
1068 #[test]
1069 fn parses_numstat_line() {
1070 use git::parse_git_numstat_line;
1071
1072 let parsed = parse_git_numstat_line("30\t2\tsrc/lib.rs");
1073
1074 assert_eq!(
1075 parsed.map(|(path, stat)| (path, stat.added, stat.removed)),
1076 Some(("src/lib.rs".to_string(), 30, 2))
1077 );
1078 }
1079
1080 #[test]
1081 fn parses_braced_numstat_rename_destination() {
1082 use git::parse_git_numstat_line;
1083
1084 let parsed = parse_git_numstat_line("4\t1\tsrc/{old => new}/file.rs");
1085
1086 assert_eq!(
1087 parsed.map(|(path, stat)| (path, stat.added, stat.removed)),
1088 Some(("src/new/file.rs".to_string(), 4, 1))
1089 );
1090 }
1091
1092 #[test]
1093 fn ignores_binary_numstat_line() {
1094 use git::parse_git_numstat_line;
1095
1096 assert_eq!(parse_git_numstat_line("-\t-\tassets/logo.png"), None);
1097 }
1098
1099 #[test]
1100 fn git_view_builders_update_props() {
1101 let tree = FileTree::new(".")
1102 .git_changed_only(true)
1103 .git_diff_stats(true);
1104
1105 assert_eq!(tree.props.change_view, FileTreeChangeView::ChangedOnly);
1106 assert!(tree.props.git_diff_stats);
1107
1108 let tree = tree.git_changed_only(false);
1109 assert_eq!(tree.props.change_view, FileTreeChangeView::AllFiles);
1110 }
1111
1112 #[test]
1113 fn highlight_changed_labels_builder_updates_props() {
1114 let tree = FileTree::new(".").highlight_changed_labels(true);
1115
1116 assert!(tree.props.highlight_changed_labels);
1117
1118 let tree = tree.highlight_changed_labels(false);
1119 assert!(!tree.props.highlight_changed_labels);
1120 }
1121
1122 #[test]
1123 fn label_style_builders_update_props() {
1124 let directory_style = Style::new().fg(Color::Blue).bold();
1125 let file_style = Style::new().fg(Color::Green);
1126
1127 let tree = FileTree::new(".")
1128 .directory_label_style(directory_style)
1129 .file_label_style(file_style);
1130
1131 assert_eq!(tree.props.directory_label_style, directory_style);
1132 assert_eq!(tree.props.file_label_style, file_style);
1133 }
1134
1135 #[test]
1136 fn item_style_builders_store_optional_styles() {
1137 let row = Style::new().bg(Color::Blue);
1138 let icon = Style::new().fg(Color::Cyan);
1139 let label = Style::new().fg(Color::Green).bold();
1140 let suffix = Style::new().dim();
1141
1142 let style = FileTreeItemStyle::new()
1143 .row(row)
1144 .icon(icon)
1145 .label(label)
1146 .suffix(suffix);
1147
1148 assert_eq!(style.row, Some(row));
1149 assert_eq!(style.icon, Some(icon));
1150 assert_eq!(style.label, Some(label));
1151 assert_eq!(style.suffix, Some(suffix));
1152 }
1153
1154 #[test]
1155 fn path_and_suffix_style_builders_update_props() {
1156 let item_style = FileTreeItemStyle::new().label(Style::new().fg(Color::Green));
1157 let suffix_style = Style::new().dim();
1158 let tree = FileTree::new("/repo")
1159 .path_style("src/main.rs", item_style)
1160 .path_styles([("src/lib.rs", item_style.suffix(suffix_style))])
1161 .change_suffix_style(suffix_style)
1162 .change_suffix_priority(FileTreeSuffixPriority::Suffix);
1163
1164 assert_eq!(tree.props.path_styles.get("src/main.rs"), Some(&item_style));
1165 assert_eq!(tree.props.change_suffix_style, suffix_style);
1166 assert_eq!(
1167 tree.props.change_suffix_priority,
1168 FileTreeSuffixPriority::Suffix
1169 );
1170
1171 let tree = tree
1172 .git_suffix_style(Style::new().italic())
1173 .git_suffix_priority(FileTreeSuffixPriority::Label);
1174 assert_eq!(tree.props.change_suffix_style, Style::new().italic());
1175 assert_eq!(
1176 tree.props.change_suffix_priority,
1177 FileTreeSuffixPriority::Label
1178 );
1179 }
1180
1181 #[test]
1182 fn crate_root_export_compiles() {
1183 let _: crate::FileTreeItemStyle = FileTreeItemStyle::new();
1184 let _: crate::FileTreeSuffixPriority = FileTreeSuffixPriority::Suffix;
1185 }
1186
1187 #[test]
1188 fn change_view_builders_update_source_agnostic_props() {
1189 let changes = vec![FileTreeChange::new(
1190 "src/main.rs",
1191 FileTreeChangeStatus::Modified,
1192 )];
1193 let tree = FileTree::new("/repo")
1194 .change_source(FileTreeChangeSource::Provided(changes.clone()))
1195 .change_view(FileTreeChangeView::ChangedOnly)
1196 .show_diff_stats(true);
1197
1198 assert_eq!(
1199 tree.props.change_source,
1200 FileTreeChangeSource::Provided(changes)
1201 );
1202 assert_eq!(tree.props.change_view, FileTreeChangeView::ChangedOnly);
1203 assert!(tree.props.git_diff_stats);
1204 }
1205
1206 #[test]
1207 fn controlled_tree_state_builders_update_props() {
1208 let tree = FileTree::new("/repo")
1209 .selected(3)
1210 .selected_path("src/main.rs")
1211 .reveal_path("src")
1212 .select_path("tests/tree.rs")
1213 .force_scroll_to_selected(true)
1214 .expanded_paths(["/repo/src", "/repo/tests"]);
1215
1216 assert_eq!(tree.props.selected, Some(3));
1217 assert_eq!(tree.props.selected_path.as_deref(), Some("src/main.rs"));
1218 assert_eq!(tree.props.reveal_path.as_deref(), Some("src"));
1219 assert_eq!(tree.props.select_path.as_deref(), Some("tests/tree.rs"));
1220 assert!(tree.props.force_scroll_to_selected);
1221 assert_eq!(
1222 tree.props.expanded_paths,
1223 Some(HashSet::from([
1224 Arc::<str>::from("/repo/src"),
1225 Arc::<str>::from("/repo/tests")
1226 ]))
1227 );
1228 }
1229
1230 #[test]
1231 fn provided_snapshot_resolves_absolute_and_rejects_outside_paths() {
1232 let changes = vec![
1233 FileTreeChange::new("/repo/src/main.rs", FileTreeChangeStatus::Modified),
1234 FileTreeChange::new("/elsewhere/ignored.rs", FileTreeChangeStatus::Deleted),
1235 FileTreeChange::new("../escape.rs", FileTreeChangeStatus::Added),
1236 ];
1237
1238 let snapshot = git::provided_change_snapshot("/repo", &changes);
1239
1240 assert_eq!(snapshot.changed_paths, vec![Arc::from("/repo/src/main.rs")]);
1241 }
1242
1243 #[test]
1244 fn provided_snapshot_accepts_absolute_paths_under_relative_root() {
1245 let root = std::path::Path::new("relative-provided-root");
1246 let absolute = std::env::current_dir()
1247 .unwrap()
1248 .join(root)
1249 .join("src/main.rs");
1250 let changes = vec![FileTreeChange::new(
1251 absolute.to_string_lossy().into_owned(),
1252 FileTreeChangeStatus::Modified,
1253 )];
1254
1255 let snapshot = git::provided_change_snapshot(root.to_str().unwrap(), &changes);
1256
1257 assert_eq!(
1258 snapshot.changed_paths,
1259 vec![Arc::from(absolute.to_string_lossy().as_ref())]
1260 );
1261 }
1262
1263 #[test]
1264 fn crate_root_exports_file_tree_change_api() {
1265 let _tree: crate::FileTree = FileTree::new("/repo");
1266 let change =
1267 crate::FileTreeChange::new("src/main.rs", crate::FileTreeChangeStatus::Modified);
1268 let _source = crate::FileTreeChangeSource::provided([change]);
1269 let _view: crate::FileTreeChangeView = crate::FileTreeGitView::ChangedOnly;
1270 let _kind: crate::FileKind = FileKind::File;
1271 }
1272
1273 #[test]
1274 fn merges_status_and_diff_stat_decorations() {
1275 use git::{
1276 GitChangeState, GitDiffStat, GitFileDecorations, GitFileStatus, insert_decoration,
1277 };
1278
1279 let mut entries = HashMap::new();
1280 let key: Arc<str> = "/repo/src/app.rs".into();
1281
1282 insert_decoration(
1283 &mut entries,
1284 key.clone(),
1285 GitFileDecorations {
1286 status: GitFileStatus::new(None, Some(GitChangeState::Modified)),
1287 diff_stat: None,
1288 direct: true,
1289 },
1290 );
1291 insert_decoration(
1292 &mut entries,
1293 key.clone(),
1294 GitFileDecorations {
1295 status: GitFileStatus::new(None, None),
1296 diff_stat: Some(GitDiffStat {
1297 added: 10,
1298 removed: 4,
1299 }),
1300 direct: false,
1301 },
1302 );
1303
1304 let decoration = entries.get(key.as_ref()).copied().unwrap();
1305 assert_eq!(decoration.status.unstaged, Some(GitChangeState::Modified));
1306 assert_eq!(
1307 decoration.diff_stat,
1308 Some(GitDiffStat {
1309 added: 10,
1310 removed: 4
1311 })
1312 );
1313 assert!(decoration.direct);
1314 }
1315}