mirror of
https://github.com/extrawurst/gitui.git
synced 2024-11-22 02:12:58 +03:00
New file tree (#718)
This commit is contained in:
parent
ca35426c6c
commit
0e31d57a33
9
Cargo.lock
generated
9
Cargo.lock
generated
@ -320,6 +320,14 @@ version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||
|
||||
[[package]]
|
||||
name = "filetree"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"pretty_assertions",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.0.1"
|
||||
@ -400,6 +408,7 @@ dependencies = [
|
||||
"crossterm",
|
||||
"dirs-next",
|
||||
"easy-cast",
|
||||
"filetree",
|
||||
"itertools",
|
||||
"log",
|
||||
"pprof",
|
||||
|
@ -21,6 +21,7 @@ keywords = [
|
||||
[dependencies]
|
||||
scopetime = { path = "./scopetime", version = "0.1" }
|
||||
asyncgit = { path = "./asyncgit", version = "0.15" }
|
||||
filetree = { path = "./filetree" }
|
||||
crossterm = { version = "0.19", features = [ "serde" ] }
|
||||
clap = { version = "2.33", default-features = false }
|
||||
tui = { version = "0.15", default-features = false, features = ['crossterm', 'serde'] }
|
||||
|
8
Makefile
8
Makefile
@ -45,18 +45,14 @@ fmt:
|
||||
|
||||
clippy:
|
||||
touch src/main.rs
|
||||
cargo clean -p gitui -p asyncgit -p scopetime
|
||||
cargo clean -p gitui -p asyncgit -p scopetime -p filetree
|
||||
cargo clippy --workspace --all-features
|
||||
|
||||
clippy-nightly:
|
||||
touch src/main.rs
|
||||
cargo clean -p gitui -p asyncgit -p scopetime
|
||||
cargo clean -p gitui -p asyncgit -p scopetime -p filetree
|
||||
cargo +nightly clippy --all-features
|
||||
|
||||
clippy-pedantic:
|
||||
cargo clean -p gitui -p asyncgit -p scopetime
|
||||
cargo clippy --all-features -- -W clippy::pedantic
|
||||
|
||||
check: fmt clippy test
|
||||
|
||||
install:
|
||||
|
@ -88,7 +88,7 @@ impl From<StatusType> for StatusShow {
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// gurantees sorting
|
||||
pub fn get_status(
|
||||
repo_path: &str,
|
||||
status_type: StatusType,
|
||||
|
@ -2,7 +2,10 @@ use super::{utils::bytes2string, CommitId};
|
||||
use crate::{error::Result, sync::utils::repo};
|
||||
use git2::{Oid, Repository, Tree};
|
||||
use scopetime::scope_time;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// `tree_files` returns a list of `FileTree`
|
||||
#[derive(Debug, PartialEq)]
|
||||
@ -15,7 +18,7 @@ pub struct TreeFile {
|
||||
id: Oid,
|
||||
}
|
||||
|
||||
///
|
||||
/// guarantees sorting the result
|
||||
pub fn tree_files(
|
||||
repo_path: &str,
|
||||
commit: CommitId,
|
||||
@ -31,9 +34,40 @@ pub fn tree_files(
|
||||
|
||||
tree_recurse(&repo, &PathBuf::from("./"), &tree, &mut files)?;
|
||||
|
||||
sort_file_list(&mut files);
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
fn sort_file_list(files: &mut Vec<TreeFile>) {
|
||||
files.sort_by(|a, b| path_cmp(&a.path, &b.path));
|
||||
}
|
||||
|
||||
// applies topologically order on paths sorting
|
||||
fn path_cmp(a: &Path, b: &Path) -> Ordering {
|
||||
let mut comp_a = a.components().into_iter().peekable();
|
||||
let mut comp_b = b.components().into_iter().peekable();
|
||||
|
||||
loop {
|
||||
let a = comp_a.next();
|
||||
let b = comp_b.next();
|
||||
|
||||
let a_is_file = comp_a.peek().is_none();
|
||||
let b_is_file = comp_b.peek().is_none();
|
||||
|
||||
if a_is_file && !b_is_file {
|
||||
return Ordering::Greater;
|
||||
} else if !a_is_file && b_is_file {
|
||||
return Ordering::Less;
|
||||
}
|
||||
|
||||
let cmp = a.cmp(&b);
|
||||
if cmp != Ordering::Equal {
|
||||
return cmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn tree_file_content(
|
||||
repo_path: &str,
|
||||
@ -109,4 +143,99 @@ mod tests {
|
||||
assert_eq!(files_c2.len(), 1);
|
||||
assert_ne!(files_c2[0], files[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sorting() {
|
||||
let mut list = vec!["file", "folder/file", "folder/afile"]
|
||||
.iter()
|
||||
.map(|f| TreeFile {
|
||||
path: PathBuf::from(f),
|
||||
filemode: 0,
|
||||
id: Oid::zero(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
sort_file_list(&mut list);
|
||||
|
||||
assert_eq!(
|
||||
list.iter()
|
||||
.map(|f| f.path.to_string_lossy())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
String::from("folder/afile"),
|
||||
String::from("folder/file"),
|
||||
String::from("file")
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sorting_folders() {
|
||||
let mut list = vec!["bfolder/file", "afolder/file"]
|
||||
.iter()
|
||||
.map(|f| TreeFile {
|
||||
path: PathBuf::from(f),
|
||||
filemode: 0,
|
||||
id: Oid::zero(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
sort_file_list(&mut list);
|
||||
|
||||
assert_eq!(
|
||||
list.iter()
|
||||
.map(|f| f.path.to_string_lossy())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
String::from("afolder/file"),
|
||||
String::from("bfolder/file"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sorting_folders2() {
|
||||
let mut list = vec!["bfolder/sub/file", "afolder/file"]
|
||||
.iter()
|
||||
.map(|f| TreeFile {
|
||||
path: PathBuf::from(f),
|
||||
filemode: 0,
|
||||
id: Oid::zero(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
sort_file_list(&mut list);
|
||||
|
||||
assert_eq!(
|
||||
list.iter()
|
||||
.map(|f| f.path.to_string_lossy())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
String::from("afolder/file"),
|
||||
String::from("bfolder/sub/file"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_cmp() {
|
||||
assert_eq!(
|
||||
path_cmp(
|
||||
&PathBuf::from("bfolder/sub/file"),
|
||||
&PathBuf::from("afolder/file")
|
||||
),
|
||||
Ordering::Greater
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_file_cmp() {
|
||||
assert_eq!(
|
||||
path_cmp(
|
||||
&PathBuf::from("a"),
|
||||
&PathBuf::from("afolder/file")
|
||||
),
|
||||
Ordering::Greater
|
||||
);
|
||||
}
|
||||
}
|
||||
|
18
filetree/Cargo.toml
Normal file
18
filetree/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "filetree"
|
||||
version = "0.1.0"
|
||||
authors = ["Stephan Dilly <dilly.stephan@gmail.com>"]
|
||||
edition = "2018"
|
||||
description = "filetree abstraction based on a sorted path list"
|
||||
homepage = "https://github.com/extrawurst/gitui"
|
||||
repository = "https://github.com/extrawurst/gitui"
|
||||
readme = "README.md"
|
||||
license-file = "LICENSE.md"
|
||||
categories = ["command-line-utilities"]
|
||||
keywords = ["gui","cli","terminal","ui"]
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "0.7"
|
1
filetree/LICENSE.md
Symbolic link
1
filetree/LICENSE.md
Symbolic link
@ -0,0 +1 @@
|
||||
../LICENSE.md
|
16
filetree/src/error.rs
Normal file
16
filetree/src/error.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use std::{num::TryFromIntError, path::PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("InvalidPath: `{0}`")]
|
||||
InvalidPath(PathBuf),
|
||||
|
||||
#[error("InvalidFilePath: `{0}`")]
|
||||
InvalidFilePath(String),
|
||||
|
||||
#[error("TryFromInt error:{0}")]
|
||||
IntConversion(#[from] TryFromIntError),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
471
filetree/src/filetree.rs
Normal file
471
filetree/src/filetree.rs
Normal file
@ -0,0 +1,471 @@
|
||||
use crate::{
|
||||
error::Result, filetreeitems::FileTreeItems,
|
||||
tree_iter::TreeIterator,
|
||||
};
|
||||
use std::{collections::BTreeSet, usize};
|
||||
|
||||
///
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum MoveSelection {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
Top,
|
||||
End,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct VisualSelection {
|
||||
pub count: usize,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
/// wraps `FileTreeItems` as a datastore and adds selection functionality
|
||||
#[derive(Default)]
|
||||
pub struct FileTree {
|
||||
items: FileTreeItems,
|
||||
selection: Option<usize>,
|
||||
// caches the absolute selection translated to visual index
|
||||
visual_selection: Option<VisualSelection>,
|
||||
}
|
||||
|
||||
impl FileTree {
|
||||
///
|
||||
pub fn new(
|
||||
list: &[&str],
|
||||
collapsed: &BTreeSet<&String>,
|
||||
) -> Result<Self> {
|
||||
let mut new_self = Self {
|
||||
items: FileTreeItems::new(list, collapsed)?,
|
||||
selection: if list.is_empty() { None } else { Some(0) },
|
||||
visual_selection: None,
|
||||
};
|
||||
new_self.visual_selection = new_self.calc_visual_selection();
|
||||
|
||||
Ok(new_self)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn collapse_but_root(&mut self) {
|
||||
self.items.collapse(0, true);
|
||||
self.items.expand(0, false);
|
||||
}
|
||||
|
||||
/// iterates visible elements starting from `start_index_visual`
|
||||
pub fn iterate(
|
||||
&self,
|
||||
start_index_visual: usize,
|
||||
max_amount: usize,
|
||||
) -> TreeIterator<'_> {
|
||||
let start = self
|
||||
.visual_index_to_absolute(start_index_visual)
|
||||
.unwrap_or_default();
|
||||
TreeIterator::new(
|
||||
self.items.iterate(start, max_amount),
|
||||
self.selection,
|
||||
)
|
||||
}
|
||||
|
||||
fn visual_index_to_absolute(
|
||||
&self,
|
||||
visual_index: usize,
|
||||
) -> Option<usize> {
|
||||
self.items
|
||||
.iterate(0, self.items.len())
|
||||
.enumerate()
|
||||
.find_map(|(i, (abs, _))| {
|
||||
if i == visual_index {
|
||||
Some(abs)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
///
|
||||
pub const fn visual_selection(&self) -> Option<&VisualSelection> {
|
||||
self.visual_selection.as_ref()
|
||||
}
|
||||
|
||||
fn calc_visual_selection(&self) -> Option<VisualSelection> {
|
||||
self.selection.map(|selection_absolute| {
|
||||
let mut count = 0;
|
||||
let mut visual_index = 0;
|
||||
for (index, _item) in
|
||||
self.items.iterate(0, self.items.len())
|
||||
{
|
||||
if selection_absolute == index {
|
||||
visual_index = count;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
}
|
||||
|
||||
VisualSelection {
|
||||
index: visual_index,
|
||||
count,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
///
|
||||
pub fn move_selection(&mut self, dir: MoveSelection) -> bool {
|
||||
self.selection.map_or(false, |selection| {
|
||||
let new_index = match dir {
|
||||
MoveSelection::Up => {
|
||||
self.selection_updown(selection, true)
|
||||
}
|
||||
MoveSelection::Down => {
|
||||
self.selection_updown(selection, false)
|
||||
}
|
||||
MoveSelection::Left => self.selection_left(selection),
|
||||
MoveSelection::Right => {
|
||||
self.selection_right(selection)
|
||||
}
|
||||
MoveSelection::Top => {
|
||||
Self::selection_start(selection)
|
||||
}
|
||||
MoveSelection::End => self.selection_end(selection),
|
||||
};
|
||||
|
||||
let changed_index =
|
||||
new_index.map(|i| i != selection).unwrap_or_default();
|
||||
|
||||
if changed_index {
|
||||
self.selection = new_index;
|
||||
self.visual_selection = self.calc_visual_selection();
|
||||
}
|
||||
|
||||
changed_index || new_index.is_some()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn collapse_recursive(&mut self) {
|
||||
if let Some(selection) = self.selection {
|
||||
self.items.collapse(selection, true);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_recursive(&mut self) {
|
||||
if let Some(selection) = self.selection {
|
||||
self.items.expand(selection, true);
|
||||
}
|
||||
}
|
||||
|
||||
const fn selection_start(current_index: usize) -> Option<usize> {
|
||||
if current_index == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(0)
|
||||
}
|
||||
}
|
||||
|
||||
fn selection_end(&self, current_index: usize) -> Option<usize> {
|
||||
let items_max = self.items.len().saturating_sub(1);
|
||||
|
||||
let mut new_index = items_max;
|
||||
|
||||
loop {
|
||||
if self.is_visible_index(new_index) {
|
||||
break;
|
||||
}
|
||||
|
||||
if new_index == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
new_index = new_index.saturating_sub(1);
|
||||
new_index = std::cmp::min(new_index, items_max);
|
||||
}
|
||||
|
||||
if new_index == current_index {
|
||||
None
|
||||
} else {
|
||||
Some(new_index)
|
||||
}
|
||||
}
|
||||
|
||||
fn selection_updown(
|
||||
&self,
|
||||
current_index: usize,
|
||||
up: bool,
|
||||
) -> Option<usize> {
|
||||
let mut index = current_index;
|
||||
|
||||
loop {
|
||||
index = {
|
||||
let new_index = if up {
|
||||
index.saturating_sub(1)
|
||||
} else {
|
||||
index.saturating_add(1)
|
||||
};
|
||||
|
||||
// when reaching usize bounds
|
||||
if new_index == index {
|
||||
break;
|
||||
}
|
||||
|
||||
if new_index >= self.items.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
new_index
|
||||
};
|
||||
|
||||
if self.is_visible_index(index) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if index == current_index {
|
||||
None
|
||||
} else {
|
||||
Some(index)
|
||||
}
|
||||
}
|
||||
|
||||
fn select_parent(
|
||||
&mut self,
|
||||
current_index: usize,
|
||||
) -> Option<usize> {
|
||||
let indent =
|
||||
self.items.tree_items[current_index].info().indent();
|
||||
|
||||
let mut index = current_index;
|
||||
|
||||
while let Some(selection) = self.selection_updown(index, true)
|
||||
{
|
||||
index = selection;
|
||||
|
||||
if self.items.tree_items[index].info().indent() < indent {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if index == current_index {
|
||||
None
|
||||
} else {
|
||||
Some(index)
|
||||
}
|
||||
}
|
||||
|
||||
fn selection_left(
|
||||
&mut self,
|
||||
current_index: usize,
|
||||
) -> Option<usize> {
|
||||
let item = &mut self.items.tree_items[current_index];
|
||||
|
||||
if item.kind().is_path() && !item.kind().is_path_collapsed() {
|
||||
self.items.collapse(current_index, false);
|
||||
return Some(current_index);
|
||||
}
|
||||
|
||||
self.select_parent(current_index)
|
||||
}
|
||||
|
||||
fn selection_right(
|
||||
&mut self,
|
||||
current_selection: usize,
|
||||
) -> Option<usize> {
|
||||
let item = &mut self.items.tree_items[current_selection];
|
||||
|
||||
if item.kind().is_path() {
|
||||
if item.kind().is_path_collapsed() {
|
||||
self.items.expand(current_selection, false);
|
||||
return Some(current_selection);
|
||||
}
|
||||
return self.selection_updown(current_selection, false);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn is_visible_index(&self, index: usize) -> bool {
|
||||
self.items
|
||||
.tree_items
|
||||
.get(index)
|
||||
.map(|item| item.info().is_visible())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{FileTree, MoveSelection};
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[test]
|
||||
fn test_selection() {
|
||||
let items = vec![
|
||||
"a/b", //
|
||||
];
|
||||
|
||||
let mut tree =
|
||||
FileTree::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
assert!(tree.move_selection(MoveSelection::Down));
|
||||
|
||||
assert_eq!(tree.selection, Some(1));
|
||||
|
||||
assert!(!tree.move_selection(MoveSelection::Down));
|
||||
|
||||
assert_eq!(tree.selection, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_selection_skips_collapsed() {
|
||||
let items = vec![
|
||||
"a/b/c", //
|
||||
"a/d", //
|
||||
];
|
||||
|
||||
//0 a/
|
||||
//1 b/
|
||||
//2 c
|
||||
//3 d
|
||||
|
||||
let mut tree =
|
||||
FileTree::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
tree.items.collapse(1, false);
|
||||
tree.selection = Some(1);
|
||||
|
||||
assert!(tree.move_selection(MoveSelection::Down));
|
||||
|
||||
assert_eq!(tree.selection, Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_selection_left_collapse() {
|
||||
let items = vec![
|
||||
"a/b/c", //
|
||||
"a/d", //
|
||||
];
|
||||
|
||||
//0 a/
|
||||
//1 b/
|
||||
//2 c
|
||||
//3 d
|
||||
|
||||
let mut tree =
|
||||
FileTree::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
tree.selection = Some(1);
|
||||
|
||||
//collapses 1
|
||||
assert!(tree.move_selection(MoveSelection::Left));
|
||||
// index will not change
|
||||
assert_eq!(tree.selection, Some(1));
|
||||
|
||||
assert!(tree.items.tree_items[1].kind().is_path_collapsed());
|
||||
assert!(!tree.items.tree_items[2].info().is_visible());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_selection_left_parent() {
|
||||
let items = vec![
|
||||
"a/b/c", //
|
||||
"a/d", //
|
||||
];
|
||||
|
||||
//0 a/
|
||||
//1 b/
|
||||
//2 c
|
||||
//3 d
|
||||
|
||||
let mut tree =
|
||||
FileTree::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
tree.selection = Some(2);
|
||||
|
||||
assert!(tree.move_selection(MoveSelection::Left));
|
||||
assert_eq!(tree.selection, Some(1));
|
||||
|
||||
assert!(tree.move_selection(MoveSelection::Left));
|
||||
assert_eq!(tree.selection, Some(1));
|
||||
|
||||
assert!(tree.move_selection(MoveSelection::Left));
|
||||
assert_eq!(tree.selection, Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_selection_right_expand() {
|
||||
let items = vec![
|
||||
"a/b/c", //
|
||||
"a/d", //
|
||||
];
|
||||
|
||||
//0 a/
|
||||
//1 b/
|
||||
//2 c
|
||||
//3 d
|
||||
|
||||
let mut tree =
|
||||
FileTree::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
tree.items.collapse(1, false);
|
||||
tree.items.collapse(0, false);
|
||||
tree.selection = Some(0);
|
||||
|
||||
assert!(tree.move_selection(MoveSelection::Right));
|
||||
assert_eq!(tree.selection, Some(0));
|
||||
assert!(!tree.items.tree_items[0].kind().is_path_collapsed());
|
||||
|
||||
assert!(tree.move_selection(MoveSelection::Right));
|
||||
assert_eq!(tree.selection, Some(1));
|
||||
assert!(tree.items.tree_items[1].kind().is_path_collapsed());
|
||||
|
||||
assert!(tree.move_selection(MoveSelection::Right));
|
||||
assert_eq!(tree.selection, Some(1));
|
||||
assert!(!tree.items.tree_items[1].kind().is_path_collapsed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_selection_top() {
|
||||
let items = vec![
|
||||
"a/b/c", //
|
||||
"a/d", //
|
||||
];
|
||||
|
||||
//0 a/
|
||||
//1 b/
|
||||
//2 c
|
||||
//3 d
|
||||
|
||||
let mut tree =
|
||||
FileTree::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
tree.selection = Some(3);
|
||||
|
||||
assert!(tree.move_selection(MoveSelection::Top));
|
||||
assert_eq!(tree.selection, Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_visible_selection() {
|
||||
let items = vec![
|
||||
"a/b/c", //
|
||||
"a/b/c2", //
|
||||
"a/d", //
|
||||
];
|
||||
|
||||
//0 a/
|
||||
//1 b/
|
||||
//2 c
|
||||
//3 c2
|
||||
//4 d
|
||||
|
||||
let mut tree =
|
||||
FileTree::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
tree.selection = Some(1);
|
||||
assert!(tree.move_selection(MoveSelection::Left));
|
||||
assert!(tree.move_selection(MoveSelection::Down));
|
||||
let s = tree.visual_selection().unwrap();
|
||||
|
||||
assert_eq!(s.count, 3);
|
||||
assert_eq!(s.index, 2);
|
||||
}
|
||||
}
|
816
filetree/src/filetreeitems.rs
Normal file
816
filetree/src/filetreeitems.rs
Normal file
@ -0,0 +1,816 @@
|
||||
use crate::{
|
||||
error::Error,
|
||||
item::{FileTreeItemKind, PathCollapsed},
|
||||
FileTreeItem,
|
||||
};
|
||||
use crate::{error::Result, treeitems_iter::TreeItemsIterator};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
path::Path,
|
||||
usize,
|
||||
};
|
||||
|
||||
///
|
||||
#[derive(Default)]
|
||||
pub struct FileTreeItems {
|
||||
pub tree_items: Vec<FileTreeItem>,
|
||||
files: usize,
|
||||
}
|
||||
|
||||
impl FileTreeItems {
|
||||
///
|
||||
pub fn new(
|
||||
list: &[&str],
|
||||
collapsed: &BTreeSet<&String>,
|
||||
) -> Result<Self> {
|
||||
let (mut items, paths) = Self::create_items(list, collapsed)?;
|
||||
|
||||
Self::fold_paths(&mut items, &paths);
|
||||
|
||||
Ok(Self {
|
||||
tree_items: items,
|
||||
files: list.len(),
|
||||
})
|
||||
}
|
||||
|
||||
fn create_items<'a>(
|
||||
list: &'a [&str],
|
||||
collapsed: &BTreeSet<&String>,
|
||||
) -> Result<(Vec<FileTreeItem>, BTreeMap<&'a Path, usize>)> {
|
||||
let mut items = Vec::with_capacity(list.len());
|
||||
let mut paths_added: BTreeMap<&Path, usize> = BTreeMap::new();
|
||||
|
||||
for e in list {
|
||||
{
|
||||
let item_path = Path::new(e);
|
||||
Self::push_dirs(
|
||||
item_path,
|
||||
&mut items,
|
||||
&mut paths_added,
|
||||
collapsed,
|
||||
)?;
|
||||
}
|
||||
|
||||
items.push(FileTreeItem::new_file(e)?);
|
||||
}
|
||||
|
||||
Ok((items, paths_added))
|
||||
}
|
||||
|
||||
/// how many individual items (files/paths) are in the list
|
||||
pub fn len(&self) -> usize {
|
||||
self.tree_items.len()
|
||||
}
|
||||
|
||||
/// how many files were added to this list
|
||||
pub const fn file_count(&self) -> usize {
|
||||
self.files
|
||||
}
|
||||
|
||||
/// iterates visible elements
|
||||
pub const fn iterate(
|
||||
&self,
|
||||
start: usize,
|
||||
max_amount: usize,
|
||||
) -> TreeItemsIterator<'_> {
|
||||
TreeItemsIterator::new(self, start, max_amount)
|
||||
}
|
||||
|
||||
fn push_dirs<'a>(
|
||||
item_path: &'a Path,
|
||||
nodes: &mut Vec<FileTreeItem>,
|
||||
// helps to only add new nodes for paths that were not added before
|
||||
// we also count the number of children a node has for later folding
|
||||
paths_added: &mut BTreeMap<&'a Path, usize>,
|
||||
collapsed: &BTreeSet<&String>,
|
||||
) -> Result<()> {
|
||||
let mut ancestors =
|
||||
item_path.ancestors().skip(1).collect::<Vec<_>>();
|
||||
ancestors.reverse();
|
||||
|
||||
for c in &ancestors {
|
||||
if c.parent().is_some() && !paths_added.contains_key(c) {
|
||||
// add node and set count to have no children
|
||||
paths_added.entry(c).or_insert(0);
|
||||
|
||||
// increase the number of children in the parent node count
|
||||
if let Some(parent) = c.parent() {
|
||||
if !parent.as_os_str().is_empty() {
|
||||
*paths_added.entry(parent).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let path_string = Self::path_to_string(c)?;
|
||||
let is_collapsed = collapsed.contains(&path_string);
|
||||
nodes.push(FileTreeItem::new_path(
|
||||
c,
|
||||
path_string,
|
||||
is_collapsed,
|
||||
)?);
|
||||
}
|
||||
}
|
||||
|
||||
// increase child count in parent node (the above ancenstor ignores the leaf component)
|
||||
if let Some(parent) = item_path.parent() {
|
||||
*paths_added.entry(parent).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn path_to_string(p: &Path) -> Result<String> {
|
||||
Ok(p.to_str()
|
||||
.map_or_else(
|
||||
|| Err(Error::InvalidPath(p.to_path_buf())),
|
||||
Ok,
|
||||
)?
|
||||
.to_string())
|
||||
}
|
||||
|
||||
pub fn collapse(&mut self, index: usize, recursive: bool) {
|
||||
if self.tree_items[index].kind().is_path() {
|
||||
self.tree_items[index].collapse_path();
|
||||
|
||||
let path = format!(
|
||||
"{}/",
|
||||
self.tree_items[index].info().full_path()
|
||||
);
|
||||
|
||||
for i in index + 1..self.tree_items.len() {
|
||||
let item = &mut self.tree_items[i];
|
||||
|
||||
if recursive && item.kind().is_path() {
|
||||
item.collapse_path();
|
||||
}
|
||||
|
||||
let item_path = &item.info().full_path();
|
||||
|
||||
if item_path.starts_with(&path) {
|
||||
item.hide();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand(&mut self, index: usize, recursive: bool) {
|
||||
if self.tree_items[index].kind().is_path() {
|
||||
self.tree_items[index].expand_path();
|
||||
|
||||
let full_path = format!(
|
||||
"{}/",
|
||||
self.tree_items[index].info().full_path()
|
||||
);
|
||||
|
||||
if recursive {
|
||||
for i in index + 1..self.tree_items.len() {
|
||||
let item = &mut self.tree_items[i];
|
||||
|
||||
if !item
|
||||
.info()
|
||||
.full_path()
|
||||
.starts_with(&full_path)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if item.kind().is_path()
|
||||
&& item.kind().is_path_collapsed()
|
||||
{
|
||||
item.expand_path();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.update_visibility(
|
||||
Some(full_path.as_str()),
|
||||
index + 1,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_visibility(
|
||||
&mut self,
|
||||
prefix: Option<&str>,
|
||||
start_idx: usize,
|
||||
set_defaults: bool,
|
||||
) {
|
||||
// if we are in any subpath that is collapsed we keep skipping over it
|
||||
let mut inner_collapsed: Option<String> = None;
|
||||
|
||||
for i in start_idx..self.tree_items.len() {
|
||||
if let Some(ref collapsed_path) = inner_collapsed {
|
||||
let p = self.tree_items[i].info().full_path();
|
||||
if p.starts_with(collapsed_path) {
|
||||
if set_defaults {
|
||||
self.tree_items[i]
|
||||
.info_mut()
|
||||
.set_visible(false);
|
||||
}
|
||||
// we are still in a collapsed inner path
|
||||
continue;
|
||||
}
|
||||
inner_collapsed = None;
|
||||
}
|
||||
|
||||
let item_kind = self.tree_items[i].kind().clone();
|
||||
let item_path = self.tree_items[i].info().full_path();
|
||||
|
||||
if matches!(item_kind, FileTreeItemKind::Path(PathCollapsed(collapsed)) if collapsed)
|
||||
{
|
||||
// we encountered an inner path that is still collapsed
|
||||
inner_collapsed = Some(format!("{}/", &item_path));
|
||||
}
|
||||
|
||||
if prefix
|
||||
.map_or(true, |prefix| item_path.starts_with(prefix))
|
||||
{
|
||||
self.tree_items[i].info_mut().set_visible(true);
|
||||
} else {
|
||||
// if we do not set defaults we can early out
|
||||
if set_defaults {
|
||||
self.tree_items[i].info_mut().set_visible(false);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fold_paths(
|
||||
items: &mut Vec<FileTreeItem>,
|
||||
paths: &BTreeMap<&Path, usize>,
|
||||
) {
|
||||
let mut i = 0;
|
||||
|
||||
while i < items.len() {
|
||||
let item = &items[i];
|
||||
if item.kind().is_path() {
|
||||
let children =
|
||||
paths.get(&Path::new(item.info().full_path()));
|
||||
|
||||
if let Some(children) = children {
|
||||
if *children == 1 {
|
||||
if i + 1 >= items.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
if items
|
||||
.get(i + 1)
|
||||
.map(|item| item.kind().is_path())
|
||||
.unwrap_or_default()
|
||||
{
|
||||
let next_item = items.remove(i + 1);
|
||||
let item_mut = &mut items[i];
|
||||
item_mut.fold(next_item);
|
||||
|
||||
let prefix = item_mut
|
||||
.info()
|
||||
.full_path()
|
||||
.to_owned();
|
||||
|
||||
Self::unindent(items, &prefix, i + 1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn unindent(
|
||||
items: &mut Vec<FileTreeItem>,
|
||||
prefix: &str,
|
||||
start: usize,
|
||||
) {
|
||||
for elem in items.iter_mut().skip(start) {
|
||||
if elem.info().full_path().starts_with(prefix) {
|
||||
elem.info_mut().unindent();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let items = vec![
|
||||
"file.txt", //
|
||||
];
|
||||
|
||||
let res =
|
||||
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
assert!(res.tree_items[0].info().is_visible());
|
||||
assert_eq!(res.tree_items[0].info().indent(), 0);
|
||||
assert_eq!(res.tree_items[0].info().path(), items[0]);
|
||||
assert_eq!(res.tree_items[0].info().full_path(), items[0]);
|
||||
|
||||
let items = vec![
|
||||
"file.txt", //
|
||||
"file2.txt", //
|
||||
];
|
||||
|
||||
let res =
|
||||
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
assert_eq!(res.tree_items.len(), 2);
|
||||
assert_eq!(res.tree_items.len(), res.len());
|
||||
assert_eq!(
|
||||
res.tree_items[1].info().path(),
|
||||
items[1].to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_path() {
|
||||
let mut items = Vec::new();
|
||||
let mut paths: BTreeMap<&Path, usize> = BTreeMap::new();
|
||||
|
||||
FileTreeItems::push_dirs(
|
||||
Path::new("a/b/c"),
|
||||
&mut items,
|
||||
&mut paths,
|
||||
&BTreeSet::new(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(*paths.get(&Path::new("a")).unwrap(), 1);
|
||||
|
||||
FileTreeItems::push_dirs(
|
||||
Path::new("a/b2/c"),
|
||||
&mut items,
|
||||
&mut paths,
|
||||
&BTreeSet::new(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(*paths.get(&Path::new("a")).unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_path2() {
|
||||
let mut items = Vec::new();
|
||||
let mut paths: BTreeMap<&Path, usize> = BTreeMap::new();
|
||||
|
||||
FileTreeItems::push_dirs(
|
||||
Path::new("a/b/c"),
|
||||
&mut items,
|
||||
&mut paths,
|
||||
&BTreeSet::new(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(*paths.get(&Path::new("a")).unwrap(), 1);
|
||||
assert_eq!(*paths.get(&Path::new("a/b")).unwrap(), 1);
|
||||
|
||||
FileTreeItems::push_dirs(
|
||||
Path::new("a/b/d"),
|
||||
&mut items,
|
||||
&mut paths,
|
||||
&BTreeSet::new(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(*paths.get(&Path::new("a")).unwrap(), 1);
|
||||
assert_eq!(*paths.get(&Path::new("a/b")).unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_folder() {
|
||||
let items = vec![
|
||||
"a/file.txt", //
|
||||
];
|
||||
|
||||
let res = FileTreeItems::new(&items, &BTreeSet::new())
|
||||
.unwrap()
|
||||
.tree_items
|
||||
.iter()
|
||||
.map(|i| i.info().full_path().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![String::from("a"), String::from("a/file.txt"),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indent() {
|
||||
let items = vec![
|
||||
"a/b/file.txt", //
|
||||
];
|
||||
|
||||
let list =
|
||||
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
|
||||
let mut res = list
|
||||
.tree_items
|
||||
.iter()
|
||||
.map(|i| (i.info().indent(), i.info().path()));
|
||||
|
||||
assert_eq!(res.next(), Some((0, "a/b")));
|
||||
assert_eq!(res.next(), Some((1, "file.txt")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indent_folder_file_name() {
|
||||
let items = vec![
|
||||
"a/b", //
|
||||
"a.txt", //
|
||||
];
|
||||
|
||||
let list =
|
||||
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
|
||||
let mut res = list
|
||||
.tree_items
|
||||
.iter()
|
||||
.map(|i| (i.info().indent(), i.info().path()));
|
||||
|
||||
assert_eq!(res.next(), Some((0, "a")));
|
||||
assert_eq!(res.next(), Some((1, "b")));
|
||||
assert_eq!(res.next(), Some((0, "a.txt")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_folder_dup() {
|
||||
let items = vec![
|
||||
"a/file.txt", //
|
||||
"a/file2.txt", //
|
||||
];
|
||||
|
||||
let tree =
|
||||
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
assert_eq!(tree.file_count(), 2);
|
||||
assert_eq!(tree.len(), 3);
|
||||
|
||||
let res = tree
|
||||
.tree_items
|
||||
.iter()
|
||||
.map(|i| i.info().full_path().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
String::from("a"),
|
||||
String::from("a/file.txt"),
|
||||
String::from("a/file2.txt"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collapse() {
|
||||
let items = vec![
|
||||
"a/file1.txt", //
|
||||
"b/file2.txt", //
|
||||
];
|
||||
|
||||
let mut tree =
|
||||
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
assert!(tree.tree_items[1].info().is_visible());
|
||||
|
||||
tree.collapse(0, false);
|
||||
|
||||
assert!(!tree.tree_items[1].info().is_visible());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iterate_collapsed() {
|
||||
let items = vec![
|
||||
"a/file1.txt", //
|
||||
"b/file2.txt", //
|
||||
];
|
||||
|
||||
let mut tree =
|
||||
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
tree.collapse(0, false);
|
||||
|
||||
let mut it = tree.iterate(0, 10);
|
||||
|
||||
assert_eq!(it.next().unwrap().0, 0);
|
||||
assert_eq!(it.next().unwrap().0, 2);
|
||||
assert_eq!(it.next().unwrap().0, 3);
|
||||
assert_eq!(it.next(), None);
|
||||
}
|
||||
|
||||
pub fn get_visibles(tree: &FileTreeItems) -> Vec<bool> {
|
||||
tree.tree_items
|
||||
.iter()
|
||||
.map(|e| e.info().is_visible())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expand() {
|
||||
let items = vec![
|
||||
"a/b/c", //
|
||||
"a/d", //
|
||||
];
|
||||
|
||||
//0 a/
|
||||
//1 b/
|
||||
//2 c
|
||||
//3 d
|
||||
|
||||
let mut tree =
|
||||
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
tree.collapse(1, false);
|
||||
|
||||
let visibles = get_visibles(&tree);
|
||||
|
||||
assert_eq!(
|
||||
visibles,
|
||||
vec![
|
||||
true, //
|
||||
true, //
|
||||
false, //
|
||||
true,
|
||||
]
|
||||
);
|
||||
|
||||
tree.expand(1, false);
|
||||
|
||||
let visibles = get_visibles(&tree);
|
||||
|
||||
assert_eq!(
|
||||
visibles,
|
||||
vec![
|
||||
true, //
|
||||
true, //
|
||||
true, //
|
||||
true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expand_bug() {
|
||||
let items = vec![
|
||||
"a/b/c", //
|
||||
"a/b2/d", //
|
||||
];
|
||||
|
||||
//0 a/
|
||||
//1 b/
|
||||
//2 c
|
||||
//3 b2/
|
||||
//4 d
|
||||
|
||||
let mut tree =
|
||||
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
tree.collapse(1, false);
|
||||
tree.collapse(0, false);
|
||||
|
||||
assert_eq!(
|
||||
get_visibles(&tree),
|
||||
vec![
|
||||
true, //
|
||||
false, //
|
||||
false, //
|
||||
false, //
|
||||
false,
|
||||
]
|
||||
);
|
||||
|
||||
tree.expand(0, false);
|
||||
|
||||
assert_eq!(
|
||||
get_visibles(&tree),
|
||||
vec![
|
||||
true, //
|
||||
true, //
|
||||
false, //
|
||||
true, //
|
||||
true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collapse_too_much() {
|
||||
let items = vec![
|
||||
"a/b", //
|
||||
"a2/c", //
|
||||
];
|
||||
|
||||
//0 a/
|
||||
//1 b
|
||||
//2 a2/
|
||||
//3 c
|
||||
|
||||
let mut tree =
|
||||
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
tree.collapse(0, false);
|
||||
|
||||
let visibles = get_visibles(&tree);
|
||||
|
||||
assert_eq!(
|
||||
visibles,
|
||||
vec![
|
||||
true, //
|
||||
false, //
|
||||
true, //
|
||||
true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expand_with_collapsed_sub_parts() {
|
||||
let items = vec![
|
||||
"a/b/c", //
|
||||
"a/d", //
|
||||
];
|
||||
|
||||
//0 a/
|
||||
//1 b/
|
||||
//2 c
|
||||
//3 d
|
||||
|
||||
let mut tree =
|
||||
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
tree.collapse(1, false);
|
||||
|
||||
let visibles = get_visibles(&tree);
|
||||
|
||||
assert_eq!(
|
||||
visibles,
|
||||
vec![
|
||||
true, //
|
||||
true, //
|
||||
false, //
|
||||
true,
|
||||
]
|
||||
);
|
||||
|
||||
tree.collapse(0, false);
|
||||
|
||||
let visibles = get_visibles(&tree);
|
||||
|
||||
assert_eq!(
|
||||
visibles,
|
||||
vec![
|
||||
true, //
|
||||
false, //
|
||||
false, //
|
||||
false,
|
||||
]
|
||||
);
|
||||
|
||||
tree.expand(0, false);
|
||||
|
||||
let visibles = get_visibles(&tree);
|
||||
|
||||
assert_eq!(
|
||||
visibles,
|
||||
vec![
|
||||
true, //
|
||||
true, //
|
||||
false, //
|
||||
true,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_merging {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_merge_simple() {
|
||||
let list = vec!["a/b/c"];
|
||||
let (mut items, paths) =
|
||||
FileTreeItems::create_items(&list, &BTreeSet::new())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(items.len(), 3);
|
||||
|
||||
FileTreeItems::fold_paths(&mut items, &paths);
|
||||
|
||||
assert_eq!(items.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_simple2() {
|
||||
let list = vec![
|
||||
"a/b/c", //
|
||||
"a/b/d",
|
||||
];
|
||||
let (mut items, paths) =
|
||||
FileTreeItems::create_items(&list, &BTreeSet::new())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(paths.len(), 2);
|
||||
assert_eq!(*paths.get(&Path::new("a")).unwrap(), 1);
|
||||
assert_eq!(*paths.get(&Path::new("a/b")).unwrap(), 2);
|
||||
assert_eq!(items.len(), 4);
|
||||
|
||||
FileTreeItems::fold_paths(&mut items, &paths);
|
||||
|
||||
assert_eq!(items.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_indent() {
|
||||
let list = vec![
|
||||
"a/b/c/d", //
|
||||
"a/e/f",
|
||||
];
|
||||
|
||||
//0:0 a/
|
||||
//1:1 b/c
|
||||
//2:2 d
|
||||
//3:1 e/
|
||||
//4:2 f
|
||||
|
||||
let (mut items, paths) =
|
||||
FileTreeItems::create_items(&list, &BTreeSet::new())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(items.len(), 6);
|
||||
|
||||
assert_eq!(paths.len(), 4);
|
||||
assert_eq!(*paths.get(&Path::new("a")).unwrap(), 2);
|
||||
assert_eq!(*paths.get(&Path::new("a/b")).unwrap(), 1);
|
||||
assert_eq!(*paths.get(&Path::new("a/b/c")).unwrap(), 1);
|
||||
assert_eq!(*paths.get(&Path::new("a/e")).unwrap(), 1);
|
||||
|
||||
FileTreeItems::fold_paths(&mut items, dbg!(&paths));
|
||||
|
||||
let indents: Vec<u8> =
|
||||
items.iter().map(|i| i.info().indent()).collect();
|
||||
assert_eq!(indents, vec![0, 1, 2, 1, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_single_paths() {
|
||||
let items = vec![
|
||||
"a/b/c", //
|
||||
"a/b/d", //
|
||||
];
|
||||
|
||||
//0 a/b/
|
||||
//1 c
|
||||
//2 d
|
||||
|
||||
let tree =
|
||||
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
let mut it = tree
|
||||
.iterate(0, 10)
|
||||
.map(|(_, item)| item.info().full_path());
|
||||
|
||||
assert_eq!(it.next().unwrap(), "a/b");
|
||||
assert_eq!(it.next().unwrap(), "a/b/c");
|
||||
assert_eq!(it.next().unwrap(), "a/b/d");
|
||||
assert_eq!(it.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_nothing() {
|
||||
let items = vec![
|
||||
"a/b/c", //
|
||||
"a/b2/d", //
|
||||
];
|
||||
|
||||
//0 a/
|
||||
//1 b/
|
||||
//2 c
|
||||
//3 b2/
|
||||
//4 d
|
||||
|
||||
let tree =
|
||||
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
|
||||
|
||||
let mut it = tree
|
||||
.iterate(0, 10)
|
||||
.map(|(_, item)| item.info().full_path());
|
||||
|
||||
assert_eq!(it.next().unwrap(), "a");
|
||||
assert_eq!(it.next().unwrap(), "a/b");
|
||||
assert_eq!(it.next().unwrap(), "a/b/c");
|
||||
assert_eq!(it.next().unwrap(), "a/b2");
|
||||
assert_eq!(it.next().unwrap(), "a/b2/d");
|
||||
assert_eq!(it.next(), None);
|
||||
}
|
||||
}
|
207
filetree/src/item.rs
Normal file
207
filetree/src/item.rs
Normal file
@ -0,0 +1,207 @@
|
||||
use crate::error::{Error, Result};
|
||||
use std::{convert::TryFrom, path::Path};
|
||||
|
||||
/// holds the information shared among all `FileTreeItem` in a `FileTree`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TreeItemInfo {
|
||||
/// indent level
|
||||
indent: u8,
|
||||
/// currently visible depending on the folder collapse states
|
||||
visible: bool,
|
||||
/// just the last path element
|
||||
path: String,
|
||||
/// the full path
|
||||
full_path: String,
|
||||
}
|
||||
|
||||
impl TreeItemInfo {
|
||||
///
|
||||
pub const fn new(
|
||||
indent: u8,
|
||||
path: String,
|
||||
full_path: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
indent,
|
||||
visible: true,
|
||||
path,
|
||||
full_path,
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub const fn is_visible(&self) -> bool {
|
||||
self.visible
|
||||
}
|
||||
|
||||
///
|
||||
pub fn full_path(&self) -> &str {
|
||||
&self.full_path
|
||||
}
|
||||
|
||||
///
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
///
|
||||
pub const fn indent(&self) -> u8 {
|
||||
self.indent
|
||||
}
|
||||
|
||||
///
|
||||
pub fn unindent(&mut self) {
|
||||
self.indent = self.indent.saturating_sub(1);
|
||||
}
|
||||
|
||||
pub fn set_visible(&mut self, visible: bool) {
|
||||
self.visible = visible;
|
||||
}
|
||||
}
|
||||
|
||||
/// attribute used to indicate the collapse/expand state of a path item
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub struct PathCollapsed(pub bool);
|
||||
|
||||
/// `FileTreeItem` can be of two kinds
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum FileTreeItemKind {
|
||||
Path(PathCollapsed),
|
||||
File,
|
||||
}
|
||||
|
||||
impl FileTreeItemKind {
|
||||
pub const fn is_path(&self) -> bool {
|
||||
matches!(self, Self::Path(_))
|
||||
}
|
||||
|
||||
pub const fn is_path_collapsed(&self) -> bool {
|
||||
match self {
|
||||
Self::Path(collapsed) => collapsed.0,
|
||||
Self::File => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `FileTreeItem` can be of two kinds: see `FileTreeItem` but shares an info
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileTreeItem {
|
||||
info: TreeItemInfo,
|
||||
kind: FileTreeItemKind,
|
||||
}
|
||||
|
||||
impl FileTreeItem {
|
||||
pub fn new_file(path: &str) -> Result<Self> {
|
||||
let item_path = Path::new(&path);
|
||||
|
||||
let indent = u8::try_from(
|
||||
item_path.ancestors().count().saturating_sub(2),
|
||||
)?;
|
||||
|
||||
let filename = item_path
|
||||
.file_name()
|
||||
.map_or_else(
|
||||
|| Err(Error::InvalidFilePath(path.to_string())),
|
||||
Ok,
|
||||
)?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
Ok(Self {
|
||||
info: TreeItemInfo::new(
|
||||
indent,
|
||||
filename,
|
||||
item_path.to_string_lossy().to_string(),
|
||||
),
|
||||
kind: FileTreeItemKind::File,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_path(
|
||||
path: &Path,
|
||||
path_string: String,
|
||||
collapsed: bool,
|
||||
) -> Result<Self> {
|
||||
let indent =
|
||||
u8::try_from(path.ancestors().count().saturating_sub(2))?;
|
||||
|
||||
let last_path_component =
|
||||
path.components().last().map_or_else(
|
||||
|| Err(Error::InvalidPath(path.to_path_buf())),
|
||||
Ok,
|
||||
)?;
|
||||
let last_path_component = last_path_component
|
||||
.as_os_str()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
Ok(Self {
|
||||
info: TreeItemInfo::new(
|
||||
indent,
|
||||
last_path_component,
|
||||
path_string,
|
||||
),
|
||||
kind: FileTreeItemKind::Path(PathCollapsed(collapsed)),
|
||||
})
|
||||
}
|
||||
|
||||
///
|
||||
pub fn fold(&mut self, next: Self) {
|
||||
self.info.path =
|
||||
format!("{}/{}", self.info.path, next.info.path);
|
||||
self.info.full_path = next.info.full_path;
|
||||
}
|
||||
|
||||
///
|
||||
pub const fn info(&self) -> &TreeItemInfo {
|
||||
&self.info
|
||||
}
|
||||
|
||||
///
|
||||
pub fn info_mut(&mut self) -> &mut TreeItemInfo {
|
||||
&mut self.info
|
||||
}
|
||||
|
||||
///
|
||||
pub const fn kind(&self) -> &FileTreeItemKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
///
|
||||
pub fn collapse_path(&mut self) {
|
||||
self.kind = FileTreeItemKind::Path(PathCollapsed(true));
|
||||
}
|
||||
|
||||
///
|
||||
pub fn expand_path(&mut self) {
|
||||
self.kind = FileTreeItemKind::Path(PathCollapsed(false));
|
||||
}
|
||||
|
||||
///
|
||||
pub fn hide(&mut self) {
|
||||
self.info.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for FileTreeItem {}
|
||||
|
||||
impl PartialEq for FileTreeItem {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.info.full_path.eq(&other.info.full_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for FileTreeItem {
|
||||
fn partial_cmp(
|
||||
&self,
|
||||
other: &Self,
|
||||
) -> Option<std::cmp::Ordering> {
|
||||
self.info.full_path.partial_cmp(&other.info.full_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for FileTreeItem {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.info.path.cmp(&other.info.path)
|
||||
}
|
||||
}
|
29
filetree/src/lib.rs
Normal file
29
filetree/src/lib.rs
Normal file
@ -0,0 +1,29 @@
|
||||
// #![forbid(missing_docs)]
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unused_imports)]
|
||||
#![deny(unused_must_use)]
|
||||
#![deny(dead_code)]
|
||||
#![deny(clippy::all, clippy::perf, clippy::nursery, clippy::pedantic)]
|
||||
#![deny(clippy::expect_used)]
|
||||
#![deny(clippy::filetype_is_file)]
|
||||
#![deny(clippy::cargo)]
|
||||
#![deny(clippy::unwrap_used)]
|
||||
#![deny(clippy::panic)]
|
||||
#![deny(clippy::match_like_matches_macro)]
|
||||
#![deny(clippy::needless_update)]
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
#![allow(clippy::must_use_candidate)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
mod error;
|
||||
mod filetree;
|
||||
mod filetreeitems;
|
||||
mod item;
|
||||
mod tree_iter;
|
||||
mod treeitems_iter;
|
||||
|
||||
pub use crate::{
|
||||
filetree::FileTree,
|
||||
filetree::MoveSelection,
|
||||
item::{FileTreeItem, TreeItemInfo},
|
||||
};
|
33
filetree/src/tree_iter.rs
Normal file
33
filetree/src/tree_iter.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use crate::{item::FileTreeItem, treeitems_iter::TreeItemsIterator};
|
||||
|
||||
pub struct TreeIterator<'a> {
|
||||
item_iter: TreeItemsIterator<'a>,
|
||||
selection: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a> TreeIterator<'a> {
|
||||
pub const fn new(
|
||||
item_iter: TreeItemsIterator<'a>,
|
||||
selection: Option<usize>,
|
||||
) -> Self {
|
||||
Self {
|
||||
item_iter,
|
||||
selection,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TreeIterator<'a> {
|
||||
type Item = (&'a FileTreeItem, bool);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.item_iter.next().map(|(index, item)| {
|
||||
(
|
||||
item,
|
||||
self.selection
|
||||
.map(|i| i == index)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
60
filetree/src/treeitems_iter.rs
Normal file
60
filetree/src/treeitems_iter.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use crate::{filetreeitems::FileTreeItems, item::FileTreeItem};
|
||||
|
||||
pub struct TreeItemsIterator<'a> {
|
||||
tree: &'a FileTreeItems,
|
||||
index: usize,
|
||||
increments: Option<usize>,
|
||||
max_amount: usize,
|
||||
}
|
||||
|
||||
impl<'a> TreeItemsIterator<'a> {
|
||||
pub const fn new(
|
||||
tree: &'a FileTreeItems,
|
||||
start: usize,
|
||||
max_amount: usize,
|
||||
) -> Self {
|
||||
TreeItemsIterator {
|
||||
max_amount,
|
||||
increments: None,
|
||||
index: start,
|
||||
tree,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TreeItemsIterator<'a> {
|
||||
type Item = (usize, &'a FileTreeItem);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.increments.unwrap_or_default() < self.max_amount {
|
||||
let items = &self.tree.tree_items;
|
||||
|
||||
let mut init = self.increments.is_none();
|
||||
|
||||
if let Some(i) = self.increments.as_mut() {
|
||||
*i += 1;
|
||||
} else {
|
||||
self.increments = Some(0);
|
||||
};
|
||||
|
||||
loop {
|
||||
if !init {
|
||||
self.index += 1;
|
||||
}
|
||||
init = false;
|
||||
|
||||
if self.index >= self.tree.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let elem = &items[self.index];
|
||||
|
||||
if elem.info().is_visible() {
|
||||
return Some((self.index, &items[self.index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
@ -417,8 +417,9 @@ impl Component for FileTreeComponent {
|
||||
_ => Ok(EventState::NotConsumed),
|
||||
}
|
||||
} else if e == self.key_config.move_down {
|
||||
Ok(self.move_selection(MoveSelection::Down))
|
||||
.map(Into::into)
|
||||
Ok(self
|
||||
.move_selection(MoveSelection::Down)
|
||||
.into())
|
||||
} else if e == self.key_config.move_up {
|
||||
Ok(self.move_selection(MoveSelection::Up).into())
|
||||
} else if e == self.key_config.home
|
||||
|
@ -24,6 +24,7 @@ mod tag_commit;
|
||||
mod textinput;
|
||||
mod utils;
|
||||
|
||||
pub use self::filetree::FileTreeComponent;
|
||||
pub use blame_file::BlameFileComponent;
|
||||
pub use branchlist::BranchListComponent;
|
||||
pub use changes::ChangesComponent;
|
||||
@ -34,7 +35,6 @@ pub use commitlist::CommitList;
|
||||
pub use create_branch::CreateBranchComponent;
|
||||
pub use diff::DiffComponent;
|
||||
pub use externaleditor::ExternalEditorComponent;
|
||||
pub use filetree::FileTreeComponent;
|
||||
pub use help::HelpComponent;
|
||||
pub use inspect_commit::InspectCommitComponent;
|
||||
pub use msg::MsgComponent;
|
||||
|
@ -1,3 +1,5 @@
|
||||
use std::{cell::Cell, collections::BTreeSet, convert::From};
|
||||
|
||||
use super::{
|
||||
visibility_blocking, CommandBlocking, CommandInfo, Component,
|
||||
DrawableComponent, EventState,
|
||||
@ -15,15 +17,21 @@ use asyncgit::{
|
||||
};
|
||||
use crossbeam_channel::Sender;
|
||||
use crossterm::event::Event;
|
||||
use filetree::{FileTree, MoveSelection};
|
||||
use tui::{
|
||||
backend::Backend, layout::Rect, text::Span, widgets::Clear, Frame,
|
||||
};
|
||||
|
||||
const FOLDER_ICON_COLLAPSED: &str = "\u{25b8}"; //▸
|
||||
const FOLDER_ICON_EXPANDED: &str = "\u{25be}"; //▾
|
||||
const EMPTY_STR: &str = "";
|
||||
|
||||
pub struct RevisionFilesComponent {
|
||||
title: String,
|
||||
theme: SharedTheme,
|
||||
// queue: Queue,
|
||||
files: Vec<TreeFile>,
|
||||
tree: FileTree,
|
||||
scroll_top: Cell<usize>,
|
||||
revision: Option<CommitId>,
|
||||
visible: bool,
|
||||
key_config: SharedKeyConfig,
|
||||
@ -40,10 +48,11 @@ impl RevisionFilesComponent {
|
||||
) -> Self {
|
||||
Self {
|
||||
title: String::new(),
|
||||
tree: FileTree::default(),
|
||||
theme,
|
||||
scroll_top: Cell::new(0),
|
||||
files: Vec::new(),
|
||||
revision: None,
|
||||
// queue: queue.clone(),
|
||||
visible: false,
|
||||
key_config,
|
||||
current_height: std::cell::Cell::new(0),
|
||||
@ -53,9 +62,16 @@ impl RevisionFilesComponent {
|
||||
///
|
||||
pub fn open(&mut self, commit: CommitId) -> Result<()> {
|
||||
self.files = sync::tree_files(CWD, commit)?;
|
||||
let filenames: Vec<&str> = self
|
||||
.files
|
||||
.iter()
|
||||
.map(|f| f.path.to_str().unwrap_or_default())
|
||||
.collect();
|
||||
self.tree = FileTree::new(&filenames, &BTreeSet::new())?;
|
||||
self.tree.collapse_but_root();
|
||||
self.revision = Some(commit);
|
||||
self.title = format!(
|
||||
"File Tree at {}",
|
||||
"File Tree at [{}]",
|
||||
self.revision
|
||||
.map(|c| c.get_short_string())
|
||||
.unwrap_or_default()
|
||||
@ -64,6 +80,39 @@ impl RevisionFilesComponent {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tree_item_to_span<'a>(
|
||||
item: &'a filetree::FileTreeItem,
|
||||
theme: &SharedTheme,
|
||||
selected: bool,
|
||||
) -> Span<'a> {
|
||||
let path = item.info().path();
|
||||
let indent = item.info().indent();
|
||||
|
||||
let indent_str = if indent == 0 {
|
||||
String::from("")
|
||||
} else {
|
||||
format!("{:w$}", " ", w = (indent as usize) * 2)
|
||||
};
|
||||
|
||||
let is_path = item.kind().is_path();
|
||||
let path_arrow = if is_path {
|
||||
if item.kind().is_path_collapsed() {
|
||||
FOLDER_ICON_COLLAPSED
|
||||
} else {
|
||||
FOLDER_ICON_EXPANDED
|
||||
}
|
||||
} else {
|
||||
EMPTY_STR
|
||||
};
|
||||
|
||||
let path = format!("{}{}{}", indent_str, path_arrow, path);
|
||||
Span::styled(path, theme.file_tree_item(is_path, selected))
|
||||
}
|
||||
|
||||
fn move_selection(&mut self, dir: MoveSelection) -> bool {
|
||||
self.tree.move_selection(dir)
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawableComponent for RevisionFilesComponent {
|
||||
@ -73,18 +122,45 @@ impl DrawableComponent for RevisionFilesComponent {
|
||||
area: Rect,
|
||||
) -> Result<()> {
|
||||
if self.is_visible() {
|
||||
let items = self.files.iter().map(|f| {
|
||||
Span::styled(
|
||||
f.path.to_string_lossy(),
|
||||
self.theme.text(true, false),
|
||||
)
|
||||
});
|
||||
let tree_height =
|
||||
usize::from(area.height.saturating_sub(2));
|
||||
|
||||
let selection = self.tree.visual_selection();
|
||||
|
||||
selection.map_or_else(
|
||||
|| self.scroll_top.set(0),
|
||||
|selection| {
|
||||
self.scroll_top.set(ui::calc_scroll_top(
|
||||
self.scroll_top.get(),
|
||||
tree_height,
|
||||
selection.index,
|
||||
))
|
||||
},
|
||||
);
|
||||
|
||||
let items = self
|
||||
.tree
|
||||
.iterate(self.scroll_top.get(), tree_height)
|
||||
.map(|(item, selected)| {
|
||||
Self::tree_item_to_span(
|
||||
item,
|
||||
&self.theme,
|
||||
selected,
|
||||
)
|
||||
});
|
||||
|
||||
f.render_widget(Clear, area);
|
||||
ui::draw_list(
|
||||
f,
|
||||
area,
|
||||
&self.title,
|
||||
// &format!(
|
||||
// "{}/{} (height: {}) (top: {})",
|
||||
// selection.index,
|
||||
// selection.count,
|
||||
// tree_height,
|
||||
// self.scroll_top.get()
|
||||
// ),
|
||||
items,
|
||||
true,
|
||||
&self.theme,
|
||||
@ -123,11 +199,39 @@ impl Component for RevisionFilesComponent {
|
||||
) -> Result<EventState> {
|
||||
if self.is_visible() {
|
||||
if let Event::Key(key) = event {
|
||||
if key == self.key_config.exit_popup {
|
||||
let consumed = if key == self.key_config.exit_popup {
|
||||
self.hide();
|
||||
}
|
||||
true
|
||||
} else if key == self.key_config.move_down {
|
||||
self.move_selection(MoveSelection::Down)
|
||||
} else if key == self.key_config.move_up {
|
||||
self.move_selection(MoveSelection::Up)
|
||||
} else if key == self.key_config.move_right {
|
||||
self.move_selection(MoveSelection::Right)
|
||||
} else if key == self.key_config.move_left {
|
||||
self.move_selection(MoveSelection::Left)
|
||||
} else if key == self.key_config.home
|
||||
|| key == self.key_config.shift_up
|
||||
{
|
||||
self.move_selection(MoveSelection::Top)
|
||||
} else if key == self.key_config.end
|
||||
|| key == self.key_config.shift_down
|
||||
{
|
||||
self.move_selection(MoveSelection::End)
|
||||
} else if key
|
||||
== self.key_config.tree_collapse_recursive
|
||||
{
|
||||
self.tree.collapse_recursive();
|
||||
true
|
||||
} else if key == self.key_config.tree_expand_recursive
|
||||
{
|
||||
self.tree.expand_recursive();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
return Ok(EventState::Consumed);
|
||||
return Ok(consumed.into());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,8 @@ pub struct KeyConfig {
|
||||
pub open_help: KeyEvent,
|
||||
pub move_left: KeyEvent,
|
||||
pub move_right: KeyEvent,
|
||||
pub tree_collapse_recursive: KeyEvent,
|
||||
pub tree_expand_recursive: KeyEvent,
|
||||
pub home: KeyEvent,
|
||||
pub end: KeyEvent,
|
||||
pub move_up: KeyEvent,
|
||||
@ -99,6 +101,8 @@ impl Default for KeyConfig {
|
||||
open_help: KeyEvent { code: KeyCode::Char('h'), modifiers: KeyModifiers::empty()},
|
||||
move_left: KeyEvent { code: KeyCode::Left, modifiers: KeyModifiers::empty()},
|
||||
move_right: KeyEvent { code: KeyCode::Right, modifiers: KeyModifiers::empty()},
|
||||
tree_collapse_recursive: KeyEvent { code: KeyCode::Left, modifiers: KeyModifiers::SHIFT},
|
||||
tree_expand_recursive: KeyEvent { code: KeyCode::Right, modifiers: KeyModifiers::SHIFT},
|
||||
home: KeyEvent { code: KeyCode::Home, modifiers: KeyModifiers::empty()},
|
||||
end: KeyEvent { code: KeyCode::End, modifiers: KeyModifiers::empty()},
|
||||
move_up: KeyEvent { code: KeyCode::Up, modifiers: KeyModifiers::empty()},
|
||||
|
@ -144,6 +144,20 @@ impl Theme {
|
||||
self.apply_select(style, selected)
|
||||
}
|
||||
|
||||
pub fn file_tree_item(
|
||||
&self,
|
||||
is_folder: bool,
|
||||
selected: bool,
|
||||
) -> Style {
|
||||
let style = if is_folder {
|
||||
Style::default()
|
||||
} else {
|
||||
Style::default().fg(self.diff_file_modified)
|
||||
};
|
||||
|
||||
self.apply_select(style, selected)
|
||||
}
|
||||
|
||||
fn apply_select(&self, style: Style, selected: bool) -> Style {
|
||||
if selected {
|
||||
style.bg(self.selection_bg)
|
||||
|
@ -40,6 +40,8 @@
|
||||
move_down: ( code: Char('j'), modifiers: ( bits: 0,),),
|
||||
page_up: ( code: Char('b'), modifiers: ( bits: 2,),),
|
||||
page_down: ( code: Char('f'), modifiers: ( bits: 2,),),
|
||||
tree_collapse_recursive: ( code: Left, modifiers: ( bits: 1,),),
|
||||
tree_expand_recursive: ( code: Right, modifiers: ( bits: 1,),),
|
||||
|
||||
shift_up: ( code: Char('K'), modifiers: ( bits: 1,),),
|
||||
shift_down: ( code: Char('J'), modifiers: ( bits: 1,),),
|
||||
|
Loading…
Reference in New Issue
Block a user