mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 17:08:21 +00:00
Fold folders up into parent if alone in directory (#288)
This commit is contained in:
parent
59c6626125
commit
adb6025c51
3 changed files with 365 additions and 89 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue