Fold folders up into parent if alone in directory (#288)

This commit is contained in:
Richard Menzies 2020-09-27 17:37:40 +01:00 committed by GitHub
parent 59c6626125
commit adb6025c51
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 365 additions and 89 deletions

View file

@ -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<Text<'b>> {
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<TextDrawInfo> 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<TextDrawInfo>, usize) {
let mut should_skip_over: usize = 0;
let mut selection_offset: usize = 0;
let mut vec_draw_text_info: Vec<TextDrawInfo> = 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<B: Backend>(
&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,

View file

@ -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<FileTreeItem>,
@ -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<usize> 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);
}
}

View file

@ -10,6 +10,10 @@ use std::{cmp, collections::BTreeSet};
pub struct StatusTree {
pub tree: FileTreeItems,
pub selection: Option<usize>,
// some folders may be folded up, this allows jumping
// over folders which are folded into their parent
pub available_selections: Vec<usize>,
}
///
@ -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<usize> {
// 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<usize> = 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));
}
}