From adb6025c5108984416b94d10c64c914e5071d96a Mon Sep 17 00:00:00 2001 From: Richard Menzies <52405405+WizardOhio24@users.noreply.github.com> Date: Sun, 27 Sep 2020 17:37:40 +0100 Subject: [PATCH] Fold folders up into parent if alone in directory (#288) --- src/components/filetree.rs | 140 +++++++++++----- src/components/utils/filetree.rs | 59 ++++--- src/components/utils/statustree.rs | 255 +++++++++++++++++++++++++++-- 3 files changed, 365 insertions(+), 89 deletions(-) diff --git a/src/components/filetree.rs b/src/components/filetree.rs index 8305d0c1..34a89838 100644 --- a/src/components/filetree.rs +++ b/src/components/filetree.rs @@ -135,23 +135,36 @@ impl FileTreeComponent { changed } + const fn item_status_char(item_type: StatusItemType) -> char { + match item_type { + StatusItemType::Modified => 'M', + StatusItemType::New => '+', + StatusItemType::Deleted => '-', + StatusItemType::Renamed => 'R', + StatusItemType::Typechange => ' ', + } + } + fn item_to_text<'b>( - item: &FileTreeItem, + string: &str, + indent: usize, + visible: bool, + file_item_kind: &FileTreeItemKind, width: u16, selected: bool, theme: &'b SharedTheme, ) -> Option> { - let indent_str = if item.info.indent == 0 { + let indent_str = if indent == 0 { String::from("") } else { - format!("{:w$}", " ", w = (item.info.indent as usize) * 2) + format!("{:w$}", " ", w = (indent as usize) * 2) }; - if !item.info.visible { + if !visible { return None; } - match &item.kind { + match file_item_kind { FileTreeItemKind::File(status_item) => { let status_char = Self::item_status_char(status_item.status); @@ -187,13 +200,13 @@ impl FileTreeComponent { " {}{}{:w$}", indent_str, collapse_char, - item.info.path, + string, w = width as usize ) } else { format!( " {}{}{}", - indent_str, collapse_char, item.info.path, + indent_str, collapse_char, string, ) }; @@ -205,17 +218,80 @@ impl FileTreeComponent { } } - const fn item_status_char(item_type: StatusItemType) -> char { - match item_type { - StatusItemType::Modified => 'M', - StatusItemType::New => '+', - StatusItemType::Deleted => '-', - StatusItemType::Renamed => 'R', - StatusItemType::Typechange => ' ', + /// Returns a Vec which is used to draw the `FileTreeComponent` correctly, + /// allowing folders to be folded up if they are alone in their directory + fn build_vec_text_draw_info_for_drawing( + &self, + ) -> (Vec, usize) { + let mut should_skip_over: usize = 0; + let mut selection_offset: usize = 0; + let mut vec_draw_text_info: Vec = vec![]; + let tree_items = self.tree.tree.items(); + for (index, item) in tree_items.iter().enumerate() { + if should_skip_over > 0 { + should_skip_over -= 1; + continue; + } + + let index_above_select = + index < self.tree.selection.unwrap_or(0); + + vec_draw_text_info.push(TextDrawInfo { + name: item.info.path.clone(), + indent: item.info.indent, + visible: item.info.visible, + item_kind: &item.kind, + }); + + let mut idx_temp = index; + + while idx_temp < tree_items.len().saturating_sub(2) + && tree_items[idx_temp].info.indent + < tree_items[idx_temp + 1].info.indent + { + // fold up the folder/file + idx_temp += 1; + should_skip_over += 1; + + // don't fold files up + if let FileTreeItemKind::File(_) = + &tree_items[idx_temp].kind + { + should_skip_over -= 1; + break; + } + + // don't fold up if more than one folder in folder + if self.tree.tree.multiple_items_at_path(idx_temp) { + should_skip_over -= 1; + break; + } else { + // There is only one item at this level (i.e only one folder in the folder), + // so do fold up + + let vec_draw_text_info_len = + vec_draw_text_info.len(); + vec_draw_text_info[vec_draw_text_info_len - 1] + .name += &(String::from("/") + + &tree_items[idx_temp].info.path); + if index_above_select { + selection_offset += 1; + } + } + } } + (vec_draw_text_info, selection_offset) } } +/// Used for drawing the `FileTreeComponent` +struct TextDrawInfo<'a> { + name: String, + indent: u8, + visible: bool, + item_kind: &'a FileTreeItemKind, +} + impl DrawableComponent for FileTreeComponent { fn draw( &self, @@ -238,21 +314,8 @@ impl DrawableComponent for FileTreeComponent { &self.theme, ); } else { - let selection_offset = - self.tree.tree.items().iter().enumerate().fold( - 0, - |acc, (idx, e)| { - let visible = e.info.visible; - let index_above_select = - idx < self.tree.selection.unwrap_or(0); - - if !visible && index_above_select { - acc + 1 - } else { - acc - } - }, - ); + let (vec_draw_text_info, selection_offset) = + self.build_vec_text_draw_info_for_drawing(); let select = self .tree @@ -267,26 +330,21 @@ impl DrawableComponent for FileTreeComponent { select, )); - let items = self - .tree - .tree - .items() + let items = vec_draw_text_info .iter() .enumerate() - .filter_map(|(idx, e)| { + .filter_map(|(index, draw_text_info)| { Self::item_to_text( - e, + &draw_text_info.name, + draw_text_info.indent as usize, + draw_text_info.visible, + draw_text_info.item_kind, r.width, - self.show_selection - && self - .tree - .selection - .map_or(false, |e| e == idx), + self.show_selection && select == index, &self.theme, ) }) .skip(self.scroll_top.get()); - ui::draw_list( f, r, diff --git a/src/components/utils/filetree.rs b/src/components/utils/filetree.rs index ce59aa20..079f826c 100644 --- a/src/components/utils/filetree.rs +++ b/src/components/utils/filetree.rs @@ -185,27 +185,6 @@ impl FileTreeItems { self.file_count } - /// - pub(crate) fn find_parent_index( - &self, - path: &str, - index: usize, - ) -> usize { - if let Some(parent_path) = Path::new(path).parent() { - let parent_path = - parent_path.to_str().expect("invalid path"); - for i in (0..=index).rev() { - let item = &self.items[i]; - let item_path = &item.info.full_path; - if item_path == parent_path { - return i; - } - } - } - - 0 - } - fn push_dirs<'a>( item_path: &'a Path, nodes: &mut Vec, @@ -232,6 +211,25 @@ impl FileTreeItems { Ok(()) } + + pub fn multiple_items_at_path(&self, index: usize) -> bool { + let tree_items = self.items(); + let mut idx_temp_inner; + if index + 2 < tree_items.len() { + idx_temp_inner = index + 1; + while idx_temp_inner < tree_items.len().saturating_sub(1) + && tree_items[index].info.indent + < tree_items[idx_temp_inner].info.indent + { + idx_temp_inner += 1; + } + } else { + return false; + } + + tree_items[idx_temp_inner].info.indent + == tree_items[index].info.indent + } } impl IndexMut for FileTreeItems { @@ -378,24 +376,25 @@ mod tests { } #[test] - fn test_find_parent() { + fn test_multiple_items_at_path() { //0 a/ //1 b/ - //2 c - //3 d + //2 c/ + //3 d + //4 e/ + //5 f let res = FileTreeItems::new( &string_vec_to_status(&[ - "a/b/c", // - "a/b/d", // + "a/b/c/d", // + "a/b/e/f", // ]), &BTreeSet::new(), ) .unwrap(); - assert_eq!( - res.find_parent_index(&String::from("a/b/c"), 3), - 1 - ); + assert_eq!(res.multiple_items_at_path(0), false); + assert_eq!(res.multiple_items_at_path(1), false); + assert_eq!(res.multiple_items_at_path(2), true); } } diff --git a/src/components/utils/statustree.rs b/src/components/utils/statustree.rs index aac54174..990f4f87 100644 --- a/src/components/utils/statustree.rs +++ b/src/components/utils/statustree.rs @@ -10,6 +10,10 @@ use std::{cmp, collections::BTreeSet}; pub struct StatusTree { pub tree: FileTreeItems, pub selection: Option, + + // some folders may be folded up, this allows jumping + // over folders which are folded into their parent + pub available_selections: Vec, } /// @@ -56,6 +60,7 @@ impl StatusTree { ); self.update_visibility(None, 0, true); + self.available_selections = self.setup_available_selections(); //NOTE: now that visibility is set we can make sure selection is visible if let Some(idx) = self.selection { @@ -65,6 +70,49 @@ impl StatusTree { Ok(()) } + /// Return which indices can be selected, taking into account that + /// some folders may be folded up into their parent + /// + /// It should be impossible to select a folder which has been folded into its parent + fn setup_available_selections(&self) -> Vec { + // use the same algorithm as in filetree build_vec_text_for_drawing function + let mut should_skip_over: usize = 0; + let mut vec_available_selections: Vec = vec![]; + let tree_items = self.tree.items(); + for index in 0..tree_items.len() { + if should_skip_over > 0 { + should_skip_over -= 1; + continue; + } + let mut idx_temp = index; + vec_available_selections.push(index); + + while idx_temp < tree_items.len().saturating_sub(2) + && tree_items[idx_temp].info.indent + < tree_items[idx_temp + 1].info.indent + { + // fold up the folder/file + idx_temp += 1; + should_skip_over += 1; + + // don't fold files up + if let FileTreeItemKind::File(_) = + &tree_items[idx_temp].kind + { + should_skip_over -= 1; + break; + } + + // don't fold up if more than one folder in folder + if self.tree.multiple_items_at_path(idx_temp) { + should_skip_over -= 1; + break; + } + } + } + vec_available_selections + } + fn find_visible_idx(&self, mut idx: usize) -> usize { while idx > 0 { if self.is_visible_index(idx) { @@ -153,30 +201,62 @@ impl StatusTree { current_index: usize, up: bool, ) -> SelectionChange { - let mut new_index = current_index; + let mut current_index_in_available_selections; + let mut cur_index_find = current_index; + if self.available_selections.is_empty() { + // Go to top + current_index_in_available_selections = 0; + } else { + loop { + if let Some(pos) = self + .available_selections + .iter() + .position(|i| *i == cur_index_find) + { + current_index_in_available_selections = pos; + break; + } else { + // Find the closest to the index, usually this shouldn't happen + if current_index == 0 { + // This should never happen + current_index_in_available_selections = 0; + break; + } + cur_index_find -= 1; + } + } + } - let items_max = self.tree.len().saturating_sub(1); + let mut new_index; loop { + // Use available_selections to go to the correct selection as + // some of the folders may be folded up new_index = if up { - new_index.saturating_sub(1) + current_index_in_available_selections = + current_index_in_available_selections + .saturating_sub(1); + self.available_selections + [current_index_in_available_selections] + } else if current_index_in_available_selections + .saturating_add(1) + <= self.available_selections.len().saturating_sub(1) + { + current_index_in_available_selections = + current_index_in_available_selections + .saturating_add(1); + self.available_selections + [current_index_in_available_selections] } else { - new_index.saturating_add(1) + // can't move down anymore + new_index = current_index; + break; }; - new_index = cmp::min(new_index, items_max); - if self.is_visible_index(new_index) { break; } - - if new_index == 0 || new_index == items_max { - // limit reached, dont update - new_index = current_index; - break; - } } - SelectionChange::new(new_index, false) } @@ -244,11 +324,7 @@ impl StatusTree { || matches!(item_kind,FileTreeItemKind::Path(PathCollapsed(collapsed)) if collapsed) { - SelectionChange::new( - self.tree - .find_parent_index(&item_path, current_selection), - false, - ) + self.selection_updown(current_selection, true) } else if matches!(item_kind, FileTreeItemKind::Path(PathCollapsed(collapsed)) if !collapsed) { @@ -677,4 +753,147 @@ mod tests { assert_eq!(res.selection, Some(3)); } + + #[test] + fn test_folders_fold_up_if_alone_in_directory() { + let items = string_vec_to_status(&[ + "a/b/c/d", // + "a/e/f/g", // + "a/h/i/j", // + ]); + + //0 a/ + //1 b/ + //2 c/ + //3 d + //4 e/ + //5 f/ + //6 g + //7 h/ + //8 i/ + //9 j + + //0 a/ + //1 b/c/ + //3 d + //4 e/f/ + //6 g + //7 h/i/ + //9 j + + let mut res = StatusTree::default(); + res.update(&items).unwrap(); + res.selection = Some(0); + + assert!(res.move_selection(MoveSelection::Down)); + assert_eq!(res.selection, Some(1)); + + assert!(res.move_selection(MoveSelection::Down)); + assert_eq!(res.selection, Some(3)); + + assert!(res.move_selection(MoveSelection::Down)); + assert_eq!(res.selection, Some(4)); + + assert!(res.move_selection(MoveSelection::Down)); + assert_eq!(res.selection, Some(6)); + + assert!(res.move_selection(MoveSelection::Down)); + assert_eq!(res.selection, Some(7)); + + assert!(res.move_selection(MoveSelection::Down)); + assert_eq!(res.selection, Some(9)); + } + + #[test] + fn test_folders_fold_up_if_alone_in_directory_2() { + let items = string_vec_to_status(&["a/b/c/d/e/f/g/h"]); + + //0 a/ + //1 b/ + //2 c/ + //3 d/ + //4 e/ + //5 f/ + //6 g/ + //7 h + + //0 a/b/c/d/e/f/g/ + //7 h + + let mut res = StatusTree::default(); + res.update(&items).unwrap(); + res.selection = Some(0); + + assert!(res.move_selection(MoveSelection::Down)); + assert_eq!(res.selection, Some(7)); + } + + #[test] + fn test_folders_fold_up_down_with_selection_left_right() { + let items = string_vec_to_status(&[ + "a/b/c/d", // + "a/e/f/g", // + "a/h/i/j", // + ]); + + //0 a/ + //1 b/ + //2 c/ + //3 d + //4 e/ + //5 f/ + //6 g + //7 h/ + //8 i/ + //9 j + + //0 a/ + //1 b/c/ + //3 d + //4 e/f/ + //6 g + //7 h/i/ + //9 j + + let mut res = StatusTree::default(); + res.update(&items).unwrap(); + res.selection = Some(0); + + assert!(res.move_selection(MoveSelection::Left)); + assert_eq!(res.selection, Some(0)); + + // These should do nothing + res.move_selection(MoveSelection::Left); + res.move_selection(MoveSelection::Left); + assert_eq!(res.selection, Some(0)); + // + assert!(res.move_selection(MoveSelection::Right)); // unfold 0 + assert_eq!(res.selection, Some(0)); + + assert!(res.move_selection(MoveSelection::Right)); // move to 1 + assert_eq!(res.selection, Some(1)); + + assert!(res.move_selection(MoveSelection::Left)); // fold 1 + assert!(res.move_selection(MoveSelection::Down)); // move to 4 + assert_eq!(res.selection, Some(4)); + + assert!(res.move_selection(MoveSelection::Left)); // fold 4 + assert!(res.move_selection(MoveSelection::Down)); // move to 7 + assert_eq!(res.selection, Some(7)); + + assert!(res.move_selection(MoveSelection::Right)); // move to 9 + assert_eq!(res.selection, Some(9)); + + assert!(res.move_selection(MoveSelection::Left)); // move to 7 + assert_eq!(res.selection, Some(7)); + + assert!(res.move_selection(MoveSelection::Left)); // folds 7 + assert_eq!(res.selection, Some(7)); + assert!(res.move_selection(MoveSelection::Left)); // move to 4 + assert_eq!(res.selection, Some(4)); + assert!(res.move_selection(MoveSelection::Left)); // move to 1 + assert_eq!(res.selection, Some(1)); + assert!(res.move_selection(MoveSelection::Left)); // move to 0 + assert_eq!(res.selection, Some(0)); + } }