diff --git a/CHANGELOG.md b/CHANGELOG.md index b220c0a9b0..fdd8737966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * changes in commit message inside external editor [[@bc-universe]](https://github.com/bc-universe) ([#1420](https://github.com/extrawurst/gitui/issues/1420)) * add no-verify option on commits to not run hooks [[@dam5h]](https://github.com/dam5h) ([#1374](https://github.com/extrawurst/gitui/issues/1374)) * allow `fetch` on status tab [[@alensiljak]](https://github.com/alensiljak) ([#1471](https://github.com/extrawurst/gitui/issues/1471)) +* Use `filetreelist` crate for the status tree. [[@abergmeier]](https://github.com/abergmeier) ([#1504](https://github.com/extrawurst/gitui/issues/1504)) ### Fixes * commit msg history ordered the wrong way ([#1445](https://github.com/extrawurst/gitui/issues/1445)) diff --git a/src/components/changes.rs b/src/components/changes.rs index 27458e8e91..b5f7ba292d 100644 --- a/src/components/changes.rs +++ b/src/components/changes.rs @@ -1,7 +1,6 @@ use super::{ - status_tree::StatusTreeComponent, - utils::filetree::{FileTreeItem, FileTreeItemKind}, - CommandBlocking, DrawableComponent, + status_tree::StatusTreeComponent, CommandBlocking, + DrawableComponent, }; use crate::{ components::{CommandInfo, Component, EventState}, @@ -17,6 +16,7 @@ use asyncgit::{ StatusItem, StatusItemType, }; use crossterm::event::Event; +use filetreelist::FileTreeItem; use std::path::Path; use tui::{backend::Backend, layout::Rect, Frame}; @@ -68,10 +68,14 @@ impl ChangesComponent { } /// - pub fn selection(&self) -> Option { + pub fn selection_ref(&self) -> Option<&FileTreeItem> { self.files.selection() } + pub fn selection_status(&self) -> Option { + self.files.selection_status() + } + /// pub fn focus_select(&mut self, focus: bool) { self.files.focus(focus); @@ -85,16 +89,28 @@ impl ChangesComponent { /// pub fn is_file_seleted(&self) -> bool { - self.files.is_file_seleted() + self.files.is_file_selected() } + /// Returns true when there was a selection. fn index_add_remove(&mut self) -> Result { - if let Some(tree_item) = self.selection() { + if let Some(tree_item) = self.selection_ref() { if self.is_working_dir { - if let FileTreeItemKind::File(i) = tree_item.kind { - let path = Path::new(i.path.as_str()); - match i.status { - StatusItemType::Deleted => { + if tree_item.kind().is_path() { + let config = + self.options.borrow().status_show_untracked(); + + //TODO: check if we can handle the one file case with it aswell + sync::stage_add_all( + &self.repo.borrow(), + tree_item.info().full_path_str(), + config, + )?; + } else { + let path = + Path::new(tree_item.info().full_path_str()); + match self.selection_status().map(|s| s.status) { + Some(StatusItemType::Deleted) => { sync::stage_addremoved( &self.repo.borrow(), path, @@ -105,16 +121,6 @@ impl ChangesComponent { path, )?, }; - } else { - let config = - self.options.borrow().status_show_untracked(); - - //TODO: check if we can handle the one file case with it aswell - sync::stage_add_all( - &self.repo.borrow(), - tree_item.info.full_path.as_str(), - config, - )?; } //TODO: this might be slow in big repos, @@ -130,7 +136,7 @@ impl ChangesComponent { } } else { // this is a staged entry, so lets unstage it - let path = tree_item.info.full_path.as_str(); + let path = tree_item.info().full_path_str(); sync::reset_stage(&self.repo.borrow(), path)?; } @@ -159,12 +165,14 @@ impl ChangesComponent { } fn dispatch_reset_workdir(&mut self) -> bool { - if let Some(tree_item) = self.selection() { - let is_folder = - matches!(tree_item.kind, FileTreeItemKind::Path(_)); + if let Some(tree_item) = self.selection_ref() { + let is_folder = tree_item.kind().is_path(); self.queue.push(InternalEvent::ConfirmAction( Action::Reset(ResetItem { - path: tree_item.info.full_path, + path: tree_item + .info() + .full_path_str() + .to_string(), is_folder, }), )); @@ -175,15 +183,16 @@ impl ChangesComponent { } fn add_to_ignore(&mut self) -> bool { - if let Some(tree_item) = self.selection() { + if let Some(tree_item) = self.selection_ref() { if let Err(e) = sync::add_to_ignore( &self.repo.borrow(), - &tree_item.info.full_path, + tree_item.info().full_path_str(), ) { self.queue.push(InternalEvent::ShowErrorMsg( format!( "ignore error:\n{}\nfile:\n{:?}", - e, tree_item.info.full_path + e, + tree_item.info().full_path() ), )); } else { @@ -218,7 +227,7 @@ impl Component for ChangesComponent { ) -> CommandBlocking { self.files.commands(out, force_all); - let some_selection = self.selection().is_some(); + let some_selection = self.selection_ref().is_some(); if self.is_working_dir { out.push(CommandInfo::new( diff --git a/src/components/compare_commits.rs b/src/components/compare_commits.rs index 11ae5c3ea6..83000a38ba 100644 --- a/src/components/compare_commits.rs +++ b/src/components/compare_commits.rs @@ -256,7 +256,7 @@ impl CompareCommitsComponent { if let Some(f) = self.details.files().selection_file() { let diff_params = DiffParams { - path: f.path.clone(), + path: f.full_path_str().to_string(), diff_type: DiffType::Commits(ids), options: DiffOptions::default(), }; @@ -265,7 +265,11 @@ impl CompareCommitsComponent { self.git_diff.last()? { if params == diff_params { - self.diff.update(f.path, false, last); + self.diff.update( + f.full_path_str().to_string(), + false, + last, + ); return Ok(()); } } @@ -293,7 +297,7 @@ impl CompareCommitsComponent { } fn can_focus_diff(&self) -> bool { - self.details.files().selection_file().is_some() + self.details.files().selection_file_ref().is_some() } fn hide_stacked(&mut self, stack: bool) { diff --git a/src/components/inspect_commit.rs b/src/components/inspect_commit.rs index 416a8ab668..8164e6a782 100644 --- a/src/components/inspect_commit.rs +++ b/src/components/inspect_commit.rs @@ -270,7 +270,7 @@ impl InspectCommitComponent { if let Some(f) = self.details.files().selection_file() { let diff_params = DiffParams { - path: f.path.clone(), + path: f.full_path_str().to_string(), diff_type: DiffType::Commit( request.commit_id, ), @@ -281,7 +281,11 @@ impl InspectCommitComponent { self.git_diff.last()? { if params == diff_params { - self.diff.update(f.path, false, last); + self.diff.update( + f.full_path_str().to_string(), + false, + last, + ); return Ok(()); } } @@ -311,7 +315,7 @@ impl InspectCommitComponent { } fn can_focus_diff(&self) -> bool { - self.details.files().selection_file().is_some() + self.details.files().selection_file_ref().is_some() } fn hide_stacked(&mut self, stack: bool) { diff --git a/src/components/mod.rs b/src/components/mod.rs index 8d1b5114d8..5edced5d48 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -65,7 +65,6 @@ pub use syntax_text::SyntaxTextComponent; pub use tag_commit::TagCommitComponent; pub use taglist::TagListComponent; pub use textinput::{InputType, TextInputComponent}; -pub use utils::filetree::FileTreeItemKind; use crate::ui::style::Theme; use anyhow::Result; diff --git a/src/components/status_tree.rs b/src/components/status_tree.rs index f4c2f837a7..9cc6de0b5e 100644 --- a/src/components/status_tree.rs +++ b/src/components/status_tree.rs @@ -1,6 +1,6 @@ use super::{ utils::{ - filetree::{FileTreeItem, FileTreeItemKind}, + filetree::Item, statustree::{MoveSelection, StatusTree}, }, BlameFileOpen, CommandBlocking, DrawableComponent, FileRevOpen, @@ -16,11 +16,10 @@ use crate::{ use anyhow::Result; use asyncgit::{hash, sync::CommitId, StatusItem, StatusItemType}; use crossterm::event::Event; +use filetreelist::{FileTreeItem, TreeItemInfo}; use std::{borrow::Cow, cell::Cell, convert::From, path::Path}; use tui::{backend::Backend, layout::Rect, text::Span, Frame}; -//TODO: use new `filetreelist` crate - /// #[allow(clippy::struct_excessive_bools)] pub struct StatusTreeComponent { @@ -81,21 +80,28 @@ impl StatusTreeComponent { } /// - pub fn selection(&self) -> Option { - self.tree.selected_item() + pub fn selection(&self) -> Option<&FileTreeItem> { + self.tree.selected_tree_item() } - /// - pub fn selection_file(&self) -> Option { + pub fn selection_file(&self) -> Option { + self.selection_file_ref().map(Clone::clone) + } + + pub fn selection_file_ref(&self) -> Option<&TreeItemInfo> { self.tree.selected_item().and_then(|f| { - if let FileTreeItemKind::File(f) = f.kind { - Some(f) - } else { + if f.is_path() { None + } else { + Some(f.info()) } }) } + pub fn selection_status(&self) -> Option { + self.tree.selected_item().and_then(Item::status) + } + /// pub fn show_selection(&mut self, show: bool) { self.show_selection = show; @@ -124,13 +130,10 @@ impl StatusTreeComponent { } /// - pub fn is_file_seleted(&self) -> bool { - self.tree.selected_item().map_or(false, |item| { - match item.kind { - FileTreeItemKind::File(_) => true, - FileTreeItemKind::Path(..) => false, - } - }) + pub fn is_file_selected(&self) -> bool { + self.tree + .selected_item() + .map_or(false, |item| !item.is_path()) } fn move_selection(&mut self, dir: MoveSelection) -> bool { @@ -158,13 +161,13 @@ impl StatusTreeComponent { fn item_to_text<'b>( string: &str, - indent: usize, - visible: bool, - file_item_kind: &FileTreeItemKind, + draw_text_info: &TextDrawInfo, width: u16, selected: bool, theme: &'b SharedTheme, ) -> Option> { + let indent = draw_text_info.indent as usize; + let visible = draw_text_info.visible; let indent_str = if indent == 0 { String::new() } else { @@ -175,55 +178,57 @@ impl StatusTreeComponent { return None; } - match file_item_kind { - FileTreeItemKind::File(status_item) => { - let status_char = - Self::item_status_char(status_item.status); - let file = Path::new(&status_item.path) - .file_name() - .and_then(std::ffi::OsStr::to_str) - .expect("invalid path."); + draw_text_info.status_item.as_ref().map_or_else( + || { + let collapse_char = if draw_text_info.path_collapsed { + '▸' + } else { + '▾' + }; let txt = if selected { format!( - "{} {}{:w$}", - status_char, + " {}{}{:w$}", indent_str, - file, + collapse_char, + string, w = width as usize ) } else { - format!("{status_char} {indent_str}{file}") + format!(" {indent_str}{collapse_char}{string}",) }; Some(Span::styled( Cow::from(txt), - theme.item(status_item.status, selected), + theme.text(true, selected), )) - } - - FileTreeItemKind::Path(path_collapsed) => { - let collapse_char = - if path_collapsed.0 { '▸' } else { '▾' }; + }, + |status_item| { + let status_char = + Self::item_status_char(status_item.status); + let file = Path::new(&status_item.path) + .file_name() + .and_then(std::ffi::OsStr::to_str) + .expect("invalid path."); let txt = if selected { format!( - " {}{}{:w$}", + "{} {}{:w$}", + status_char, indent_str, - collapse_char, - string, + file, w = width as usize ) } else { - format!(" {indent_str}{collapse_char}{string}",) + format!("{status_char} {indent_str}{file}") }; Some(Span::styled( Cow::from(txt), - theme.text(true, selected), + theme.item(status_item.status, selected), )) - } - } + }, + ) } /// Returns a Vec which is used to draw the `FileTreeComponent` correctly, @@ -246,39 +251,32 @@ impl StatusTreeComponent { let index_above_select = index < self.tree.selection.unwrap_or(0); - if !item.info.visible && index_above_select { + if !item.is_visible() && index_above_select { selection_offset_visible += 1; } vec_draw_text_info.push(TextDrawInfo { - name: item.info.path.clone(), - indent: item.info.indent, - visible: item.info.visible, - item_kind: &item.kind, + name: item.info().path_str().to_string(), + indent: item.info().indent(), + visible: item.info().is_visible(), + status_item: item.status(), + path_collapsed: item.is_path_collapsed(), }); 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 + && 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 - else if self - .tree - .tree - .multiple_items_at_path(idx_temp) + if !tree_items[idx_temp].is_path() + || self.tree.tree.multiple_items_at_path(idx_temp) { should_skip_over -= 1; break; @@ -290,7 +288,7 @@ impl StatusTreeComponent { 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); + + tree_items[idx_temp].info().path_str()); if index_above_select { selection_offset += 1; } @@ -305,11 +303,12 @@ impl StatusTreeComponent { } /// Used for drawing the `FileTreeComponent` -struct TextDrawInfo<'a> { +struct TextDrawInfo { name: String, indent: u8, visible: bool, - item_kind: &'a FileTreeItemKind, + status_item: Option, + path_collapsed: bool, } impl DrawableComponent for StatusTreeComponent { @@ -362,9 +361,7 @@ impl DrawableComponent for StatusTreeComponent { .filter_map(|(index, draw_text_info)| { Self::item_to_text( &draw_text_info.name, - draw_text_info.indent as usize, - draw_text_info.visible, - draw_text_info.item_kind, + draw_text_info, r.width, self.show_selection && select == index, &self.theme, @@ -404,7 +401,7 @@ impl Component for StatusTreeComponent { out.push( CommandInfo::new( strings::commands::blame_file(&self.key_config), - self.selection_file().is_some(), + self.selection_file_ref().is_some(), self.focused || force_all, ) .order(order::RARE_ACTION), @@ -415,7 +412,7 @@ impl Component for StatusTreeComponent { strings::commands::open_file_history( &self.key_config, ), - self.selection_file().is_some(), + self.selection_file_ref().is_some(), self.focused || force_all, ) .order(order::RARE_ACTION), @@ -424,7 +421,7 @@ impl Component for StatusTreeComponent { out.push( CommandInfo::new( strings::commands::edit_item(&self.key_config), - self.selection_file().is_some(), + self.selection_file_ref().is_some(), self.focused || force_all, ) .order(order::RARE_ACTION), @@ -443,7 +440,9 @@ impl Component for StatusTreeComponent { queue.push(InternalEvent::OpenPopup( StackablePopupOpen::BlameFile( BlameFileOpen { - file_path: status_item.path, + file_path: status_item + .full_path_str() + .to_string(), commit_id: self.revision, selection: None, }, @@ -462,7 +461,9 @@ impl Component for StatusTreeComponent { queue.push(InternalEvent::OpenPopup( StackablePopupOpen::FileRevlog( FileRevOpen::new( - status_item.path, + status_item + .full_path_str() + .to_string(), ), ), )); @@ -475,7 +476,11 @@ impl Component for StatusTreeComponent { if let Some(queue) = &self.queue { queue.push( InternalEvent::OpenExternalEditor( - Some(status_item.path), + Some( + status_item + .full_path_str() + .to_string(), + ), ), ); } diff --git a/src/components/utils/filetree.rs b/src/components/utils/filetree.rs index 8bf144f70d..2fee2d42e5 100644 --- a/src/components/utils/filetree.rs +++ b/src/components/utils/filetree.rs @@ -1,139 +1,60 @@ -//TODO: remove in favour of new `filetreelist` crate - -use anyhow::{bail, Result}; -use asyncgit::StatusItem; +use anyhow::Result; +use asyncgit::{StatusItem, StatusItemType}; use std::{ collections::BTreeSet, - convert::TryFrom, - ffi::OsStr, ops::{Index, IndexMut}, path::Path, }; -/// holds the information shared among all `FileTreeItem` in a `FileTree` -#[derive(Debug, Clone)] -pub struct TreeItemInfo { - /// indent level - pub indent: u8, - /// currently visible depending on the folder collapse states - pub visible: bool, - /// just the last path element - pub path: String, - /// the full path - pub full_path: String, -} +use filetreelist::{FileTreeItem, TreeItemInfo}; -impl TreeItemInfo { - const fn new( - indent: u8, - path: String, - full_path: String, - ) -> Self { - Self { - indent, - visible: true, - path, - full_path, - } - } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Item { + tree_item: FileTreeItem, + status_item_type: Option, } -/// attribute used to indicate the collapse/expand state of a path item -#[derive(PartialEq, Eq, Debug, Copy, Clone)] -pub struct PathCollapsed(pub bool); - -/// `FileTreeItem` can be of two kinds -#[derive(PartialEq, Eq, Debug, Clone)] -pub enum FileTreeItemKind { - Path(PathCollapsed), - File(StatusItem), -} +impl Item { + pub fn collapse_path(&mut self) { + self.tree_item.collapse_path(); + } -/// `FileTreeItem` can be of two kinds: see `FileTreeItem` but shares an info -#[derive(Debug, Clone)] -pub struct FileTreeItem { - pub info: TreeItemInfo, - pub kind: FileTreeItemKind, -} + pub fn expand_path(&mut self) { + self.tree_item.expand_path(); + } -impl FileTreeItem { - fn new_file(item: &StatusItem) -> Result { - let item_path = Path::new(&item.path); - let indent = u8::try_from( - item_path.ancestors().count().saturating_sub(2), - )?; - - let name = item_path - .file_name() - .map(OsStr::to_string_lossy) - .map(|x| x.to_string()); - - match name { - Some(path) => Ok(Self { - info: TreeItemInfo::new( - indent, - path, - item.path.clone(), - ), - kind: FileTreeItemKind::File(item.clone()), - }), - None => bail!("invalid file name {:?}", item), - } + pub fn info(&self) -> &TreeItemInfo { + self.tree_item.info() } - fn new_path( - path: &Path, - path_string: String, - collapsed: bool, - ) -> Result { - let indent = - u8::try_from(path.ancestors().count().saturating_sub(2))?; - - match path - .components() - .last() - .map(std::path::Component::as_os_str) - .map(OsStr::to_string_lossy) - .map(String::from) - { - Some(path) => Ok(Self { - info: TreeItemInfo::new(indent, path, path_string), - kind: FileTreeItemKind::Path(PathCollapsed( - collapsed, - )), - }), - None => bail!("failed to create item from path"), - } + pub fn is_path(&self) -> bool { + self.tree_item.kind().is_path() } -} -impl Eq for FileTreeItem {} + pub fn is_path_collapsed(&self) -> bool { + self.tree_item.kind().is_path_collapsed() + } -impl PartialEq for FileTreeItem { - fn eq(&self, other: &Self) -> bool { - self.info.full_path.eq(&other.info.full_path) + pub fn is_visible(&self) -> bool { + self.tree_item.info().is_visible() } -} -impl PartialOrd for FileTreeItem { - fn partial_cmp( - &self, - other: &Self, - ) -> Option { - self.info.full_path.partial_cmp(&other.info.full_path) + pub fn status(&self) -> Option { + self.status_item_type.map(|s| StatusItem { + path: self.info().full_path_str().to_string(), + status: s, + }) } -} -impl Ord for FileTreeItem { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.info.path.cmp(&other.info.path) + pub fn set_visible(&mut self, visible: bool) { + self.tree_item.info_mut().set_visible(visible); } } /// #[derive(Default)] pub struct FileTreeItems { - items: Vec, + items: Vec, file_count: usize, } @@ -141,24 +62,26 @@ impl FileTreeItems { /// pub(crate) fn new( list: &[StatusItem], - collapsed: &BTreeSet<&String>, + collapsed: &BTreeSet<&str>, ) -> Result { let mut items = Vec::with_capacity(list.len()); let mut paths_added = BTreeSet::new(); for e in list { - { - let item_path = Path::new(&e.path); - - Self::push_dirs( - item_path, - &mut items, - &mut paths_added, - collapsed, - )?; - } - - items.push(FileTreeItem::new_file(e)?); + let item_path = Path::new(&e.path); + + Self::push_dirs( + item_path, + &mut items, + &mut paths_added, + collapsed, + )?; + let tree_item = FileTreeItem::new_file(item_path)?; + let status_item_type = Some(e.status); + items.push(Item { + tree_item, + status_item_type, + }); } Ok(Self { @@ -167,8 +90,15 @@ impl FileTreeItems { }) } + pub(crate) fn index_tree_item( + &self, + idx: usize, + ) -> &FileTreeItem { + &self.items[idx].tree_item + } + /// - pub(crate) const fn items(&self) -> &Vec { + pub(crate) const fn items(&self) -> &Vec { &self.items } @@ -184,9 +114,10 @@ impl FileTreeItems { /// pub(crate) fn find_parent_index(&self, index: usize) -> usize { - let item_indent = &self.items[index].info.indent; + let item_indent = &self.items[index].info().indent(); let mut parent_index = index; - while item_indent <= &self.items[parent_index].info.indent { + while item_indent <= &self.items[parent_index].info().indent() + { if parent_index == 0 { return 0; } @@ -198,9 +129,9 @@ impl FileTreeItems { fn push_dirs<'a>( item_path: &'a Path, - nodes: &mut Vec, + nodes: &mut Vec, paths_added: &mut BTreeSet<&'a Path>, - collapsed: &BTreeSet<&String>, + collapsed: &BTreeSet<&str>, ) -> Result<()> { let mut ancestors = { item_path.ancestors().skip(1).collect::>() }; @@ -212,12 +143,15 @@ impl FileTreeItems { //TODO: get rid of expect let path_string = String::from(c.to_str().expect("invalid path")); - let is_collapsed = collapsed.contains(&path_string); - nodes.push(FileTreeItem::new_path( - c, - path_string, - is_collapsed, - )?); + let is_collapsed = + collapsed.contains(path_string.as_str()); + + let tree_item = + FileTreeItem::new_path(c, is_collapsed)?; + nodes.push(Item { + tree_item, + status_item_type: None, + }); } } @@ -230,8 +164,8 @@ impl FileTreeItems { 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 + && tree_items[index].info().indent() + < tree_items[idx_temp_inner].info().indent() { idx_temp_inner += 1; } @@ -239,8 +173,8 @@ impl FileTreeItems { return false; } - tree_items[idx_temp_inner].info.indent - == tree_items[index].info.indent + tree_items[idx_temp_inner].info().indent() + == tree_items[index].info().indent() } } @@ -251,7 +185,7 @@ impl IndexMut for FileTreeItems { } impl Index for FileTreeItems { - type Output = FileTreeItem; + type Output = Item; fn index(&self, idx: usize) -> &Self::Output { &self.items[idx] @@ -282,16 +216,14 @@ mod tests { let res = FileTreeItems::new(&items, &BTreeSet::new()).unwrap(); + let expected_tree_item = + FileTreeItem::new_file(Path::new(&items[0].path)) + .unwrap(); assert_eq!( res.items, - vec![FileTreeItem { - info: TreeItemInfo { - path: items[0].path.clone(), - full_path: items[0].path.clone(), - indent: 0, - visible: true, - }, - kind: FileTreeItemKind::File(items[0].clone()) + vec![Item { + tree_item: expected_tree_item, + status_item_type: Some(items[0].status), }] ); @@ -304,7 +236,10 @@ mod tests { FileTreeItems::new(&items, &BTreeSet::new()).unwrap(); assert_eq!(res.items.len(), 2); - assert_eq!(res.items[1].info.path, items[1].path); + assert_eq!( + res.items[1].info().path_str().to_string(), + items[1].path + ); } #[test] @@ -317,7 +252,7 @@ mod tests { .unwrap() .items .iter() - .map(|i| i.info.full_path.clone()) + .map(|i| i.info().full_path_str().to_string()) .collect::>(); assert_eq!( @@ -337,7 +272,7 @@ mod tests { let mut res = list .items .iter() - .map(|i| (i.info.indent, i.info.path.as_str())); + .map(|i| (i.info().indent(), i.info().path_str())); assert_eq!(res.next(), Some((0, "a"))); assert_eq!(res.next(), Some((1, "b"))); @@ -356,7 +291,7 @@ mod tests { let mut res = list .items .iter() - .map(|i| (i.info.indent, i.info.path.as_str())); + .map(|i| (i.info().indent(), i.info().path_str())); assert_eq!(res.next(), Some((0, "a"))); assert_eq!(res.next(), Some((1, "b"))); @@ -374,7 +309,7 @@ mod tests { .unwrap() .items .iter() - .map(|i| i.info.full_path.clone()) + .map(|i| i.info().full_path_str().to_string()) .collect::>(); assert_eq!( diff --git a/src/components/utils/statustree.rs b/src/components/utils/statustree.rs index 925958430e..9c6043e52f 100644 --- a/src/components/utils/statustree.rs +++ b/src/components/utils/statustree.rs @@ -1,11 +1,8 @@ -use super::filetree::{ - FileTreeItem, FileTreeItemKind, FileTreeItems, PathCollapsed, -}; +use super::filetree::{FileTreeItems, Item}; use anyhow::Result; use asyncgit::StatusItem; -use std::{cmp, collections::BTreeSet}; - -//TODO: use new `filetreelist` crate +use filetreelist::FileTreeItem; +use std::{cmp, collections::BTreeSet, path::Path}; /// #[derive(Default)] @@ -45,8 +42,9 @@ impl StatusTree { pub fn update(&mut self, list: &[StatusItem]) -> Result<()> { let last_collapsed = self.all_collapsed(); - let last_selection = - self.selected_item().map(|e| e.info.full_path); + let last_selection = self + .selected_item() + .map(|e| e.info().full_path_str().to_string()); let last_selection_index = self.selection.unwrap_or(0); self.tree = FileTreeItems::new(list, &last_collapsed)?; @@ -90,17 +88,15 @@ impl StatusTree { 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 + && 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 - { + if !tree_items[idx_temp].is_path() { should_skip_over -= 1; break; } @@ -155,8 +151,12 @@ impl StatusTree { } /// - pub fn selected_item(&self) -> Option { - self.selection.map(|i| self.tree[i].clone()) + pub fn selected_item(&self) -> Option<&Item> { + self.selection.map(|i| &self.tree[i]) + } + + pub fn selected_tree_item(&self) -> Option<&FileTreeItem> { + self.selection.map(|i| self.tree.index_tree_item(i)) } /// @@ -164,17 +164,14 @@ impl StatusTree { self.tree.items().is_empty() } - fn all_collapsed(&self) -> BTreeSet<&String> { + fn all_collapsed(&self) -> BTreeSet<&str> { let mut res = BTreeSet::new(); for i in self.tree.items() { - if let FileTreeItemKind::Path(PathCollapsed(collapsed)) = - i.kind - { - if collapsed { - res.insert(&i.info.full_path); - } + if !i.is_path_collapsed() { + continue; } + res.insert(i.info().full_path_str()); } res @@ -190,7 +187,7 @@ impl StatusTree { } if let Ok(i) = self.tree.items().binary_search_by(|e| { - e.info.full_path.as_str().cmp(last_selection) + e.info().full_path_str().cmp(last_selection) }) { return Some(i); } @@ -284,31 +281,25 @@ impl StatusTree { } fn is_visible_index(&self, idx: usize) -> bool { - self.tree[idx].info.visible + self.tree[idx].info().is_visible() } fn selection_right( &mut self, current_selection: usize, ) -> SelectionChange { - let item_kind = self.tree[current_selection].kind.clone(); - let item_path = - self.tree[current_selection].info.full_path.clone(); - - match item_kind { - FileTreeItemKind::Path(PathCollapsed(collapsed)) - if collapsed => - { + let item = &self.tree[current_selection]; + let item_path = self.tree[current_selection] + .info() + .full_path_str() + .to_string(); + + if item.is_path() { + if item.is_path_collapsed() { self.expand(&item_path, current_selection); return SelectionChange::new(current_selection, true); } - FileTreeItemKind::Path(PathCollapsed(collapsed)) - if !collapsed => - { - return self - .selection_updown(current_selection, false); - } - _ => (), + return self.selection_updown(current_selection, false); } SelectionChange::new(current_selection, false) @@ -318,14 +309,10 @@ impl StatusTree { &mut self, current_selection: usize, ) -> SelectionChange { - let item_kind = self.tree[current_selection].kind.clone(); - let item_path = - self.tree[current_selection].info.full_path.clone(); - - if matches!(item_kind, FileTreeItemKind::File(_)) - || matches!(item_kind,FileTreeItemKind::Path(PathCollapsed(collapsed)) - if collapsed) - { + let item = self.tree[current_selection].clone(); + let item_path = item.info().full_path(); + + if !item.is_path() || item.is_path_collapsed() { let mut cur_parent = self.tree.find_parent_index(current_selection); while !self.available_selections.contains(&cur_parent) @@ -334,31 +321,27 @@ impl StatusTree { cur_parent = self.tree.find_parent_index(cur_parent); } SelectionChange::new(cur_parent, false) - } else if matches!(item_kind, FileTreeItemKind::Path(PathCollapsed(collapsed)) - if !collapsed) - { - self.collapse(&item_path, current_selection); + } else if !item.is_path_collapsed() { + self.collapse(item_path, current_selection); SelectionChange::new(current_selection, true) } else { SelectionChange::new(current_selection, false) } } - fn collapse(&mut self, path: &str, index: usize) { - if let FileTreeItemKind::Path(PathCollapsed( - ref mut collapsed, - )) = self.tree[index].kind - { - *collapsed = true; + fn collapse(&mut self, path: &Path, index: usize) { + if self.tree[index].is_path() { + self.tree[index].collapse_path(); } - let path = format!("{path}/"); + let up = path.to_str().expect("Invalid unicode in path"); + let path = format!("{up}/"); for i in index + 1..self.tree.len() { let item = &mut self.tree[i]; - let item_path = &item.info.full_path; + let item_path = item.info().full_path(); if item_path.starts_with(&path) { - item.info.visible = false; + item.set_visible(false); } else { return; } @@ -366,11 +349,8 @@ impl StatusTree { } fn expand(&mut self, path: &str, current_index: usize) { - if let FileTreeItemKind::Path(PathCollapsed( - ref mut collapsed, - )) = self.tree[current_index].kind - { - *collapsed = false; + if self.tree[current_index].is_path_collapsed() { + self.tree[current_index].expand_path(); } let path = format!("{path}/"); @@ -393,10 +373,11 @@ impl StatusTree { for i in start_idx..self.tree.len() { if let Some(ref collapsed_path) = inner_collapsed { - let p: &String = &self.tree[i].info.full_path; + let p = + self.tree[i].info().full_path_str().to_string(); if p.starts_with(collapsed_path) { if set_defaults { - self.tree[i].info.visible = false; + self.tree[i].set_visible(false); } // we are still in a collapsed inner path continue; @@ -404,23 +385,22 @@ impl StatusTree { inner_collapsed = None; } - let item_kind = self.tree[i].kind.clone(); - let item_path = &self.tree[i].info.full_path; + let item = &self.tree[i]; + let item_path = self.tree[i].info().full_path_str(); - if matches!(item_kind, FileTreeItemKind::Path(PathCollapsed(collapsed)) if collapsed) - { + if item.is_path_collapsed() { // we encountered an inner path that is still collapsed - inner_collapsed = Some(format!("{}/", &item_path)); + inner_collapsed = Some(format!("{item_path}/")); } if prefix .map_or(true, |prefix| item_path.starts_with(prefix)) { - self.tree[i].info.visible = true; + self.tree[i].set_visible(true); } else { // if we do not set defaults we can early out if set_defaults { - self.tree[i].info.visible = false; + self.tree[i].set_visible(false); } else { return; } @@ -448,7 +428,7 @@ mod tests { tree.tree .items() .iter() - .map(|e| e.info.visible) + .map(|e| e.info().is_visible()) .collect::>() } @@ -497,7 +477,7 @@ mod tests { let mut res = StatusTree::default(); res.update(&string_vec_to_status(&["a/b", "c"])).unwrap(); - res.collapse("a/b", 0); + res.collapse(Path::new("a/b"), 0); res.selection = Some(2); @@ -525,7 +505,7 @@ mod tests { ])) .unwrap(); - res.collapse("a", 0); + res.collapse(Path::new("a"), 0); assert_eq!( res.all_collapsed().iter().collect::>(), @@ -579,7 +559,7 @@ mod tests { let mut res = StatusTree::default(); res.update(&items).unwrap(); - res.collapse(&String::from("a/b"), 1); + res.collapse(Path::new("a/b"), 1); let visibles = get_visibles(&res); @@ -624,8 +604,8 @@ mod tests { let mut res = StatusTree::default(); res.update(&items).unwrap(); - res.collapse(&String::from("b"), 1); - res.collapse(&String::from("a"), 0); + res.collapse(Path::new("b"), 1); + res.collapse(Path::new("a"), 0); assert_eq!( get_visibles(&res), @@ -667,7 +647,7 @@ mod tests { let mut res = StatusTree::default(); res.update(&items).unwrap(); - res.collapse(&String::from("a"), 0); + res.collapse(Path::new("a"), 0); let visibles = get_visibles(&res); @@ -697,7 +677,7 @@ mod tests { let mut res = StatusTree::default(); res.update(&items).unwrap(); - res.collapse(&String::from("a/b"), 1); + res.collapse(Path::new("a/b"), 1); let visibles = get_visibles(&res); @@ -711,7 +691,7 @@ mod tests { ] ); - res.collapse(&String::from("a"), 0); + res.collapse(Path::new("a"), 0); let visibles = get_visibles(&res); @@ -754,7 +734,7 @@ mod tests { let mut res = StatusTree::default(); res.update(&items).unwrap(); - res.collapse(&String::from("a/b"), 1); + res.collapse(Path::new("a/b"), 1); res.selection = Some(1); assert!(res.move_selection(MoveSelection::Down)); diff --git a/src/tabs/status.rs b/src/tabs/status.rs index 428daef00e..579da87bc9 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -4,7 +4,6 @@ use crate::{ command_pump, event_pump, visibility_blocking, ChangesComponent, CommandBlocking, CommandInfo, Component, DiffComponent, DrawableComponent, EventState, - FileTreeItemKind, }, keys::{key_match, SharedKeyConfig}, options::SharedOptions, @@ -386,9 +385,12 @@ impl Status { DiffTarget::WorkingDir => (&self.index_wd, false), }; - if let Some(item) = idx.selection() { - if let FileTreeItemKind::File(i) = item.kind { - return Some((i.path, is_stage)); + if let Some(item) = idx.selection_ref() { + if !item.kind().is_path() { + return Some(( + item.info().full_path_str().to_string(), + is_stage, + )); } } None