forked from Mirrors/helix
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2730 lines
67 KiB
Rust
2730 lines
67 KiB
Rust
use std::{cmp::Ordering, sync::Arc};
|
|
|
|
use anyhow::Result;
|
|
use helix_view::{icons::Icon, theme::Modifier};
|
|
|
|
use crate::{
|
|
compositor::{Component, Context, EventResult},
|
|
ctrl, key, shift, ui,
|
|
};
|
|
use helix_core::movement::Direction;
|
|
use helix_view::{
|
|
graphics::Rect,
|
|
input::{Event, KeyEvent},
|
|
};
|
|
use tui::buffer::Buffer as Surface;
|
|
|
|
use super::Prompt;
|
|
|
|
pub trait TreeViewItem: Sized + Ord {
|
|
type Params: Default;
|
|
|
|
fn name(&self) -> String;
|
|
fn is_parent(&self) -> bool;
|
|
|
|
fn filter(&self, s: &str) -> bool {
|
|
self.name().to_lowercase().contains(&s.to_lowercase())
|
|
}
|
|
|
|
fn get_children(&self) -> Result<Vec<Self>>;
|
|
}
|
|
|
|
fn tree_item_cmp<T: TreeViewItem>(item1: &T, item2: &T) -> Ordering {
|
|
T::cmp(item1, item2)
|
|
}
|
|
|
|
fn vec_to_tree<T: TreeViewItem>(mut items: Vec<T>) -> Vec<Tree<T>> {
|
|
items.sort();
|
|
index_elems(
|
|
0,
|
|
items
|
|
.into_iter()
|
|
.map(|item| Tree::new(item, vec![]))
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
pub enum TreeOp {
|
|
Noop,
|
|
GetChildsAndInsert,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub struct Tree<T> {
|
|
item: T,
|
|
parent_index: Option<usize>,
|
|
index: usize,
|
|
children: Vec<Self>,
|
|
|
|
/// Why do we need this property?
|
|
/// Can't we just use `!children.is_empty()`?
|
|
///
|
|
/// Because we might have for example an open folder that is empty,
|
|
/// and user just added a new file under that folder,
|
|
/// and the user refreshes the whole tree.
|
|
///
|
|
/// Without `open`, we will not refresh any node without children,
|
|
/// and thus the folder still appears empty after refreshing.
|
|
is_opened: bool,
|
|
}
|
|
|
|
impl<T: Clone> Clone for Tree<T> {
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
item: self.item.clone(),
|
|
index: self.index,
|
|
children: self.children.clone(),
|
|
is_opened: self.is_opened,
|
|
parent_index: self.parent_index,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct TreeIter<'a, T> {
|
|
current_index_forward: usize,
|
|
current_index_reverse: isize,
|
|
tree: &'a Tree<T>,
|
|
}
|
|
|
|
impl<'a, T> Iterator for TreeIter<'a, T> {
|
|
type Item = &'a Tree<T>;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
let index = self.current_index_forward;
|
|
if index > self.tree.len().saturating_sub(1) {
|
|
None
|
|
} else {
|
|
self.current_index_forward = self.current_index_forward.saturating_add(1);
|
|
self.tree.get(index)
|
|
}
|
|
}
|
|
|
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
|
(self.tree.len(), Some(self.tree.len()))
|
|
}
|
|
}
|
|
|
|
impl<'a, T> DoubleEndedIterator for TreeIter<'a, T> {
|
|
fn next_back(&mut self) -> Option<Self::Item> {
|
|
let index = self.current_index_reverse;
|
|
if index < 0 {
|
|
None
|
|
} else {
|
|
self.current_index_reverse = self.current_index_reverse.saturating_sub(1);
|
|
self.tree.get(index as usize)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a, T> ExactSizeIterator for TreeIter<'a, T> {}
|
|
|
|
impl<T: TreeViewItem> Tree<T> {
|
|
fn open(&mut self) -> Result<()> {
|
|
if self.item.is_parent() {
|
|
self.children = self.get_children()?;
|
|
self.is_opened = true;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn close(&mut self) {
|
|
self.is_opened = false;
|
|
self.children = vec![];
|
|
}
|
|
|
|
fn refresh(&mut self) -> Result<()> {
|
|
if !self.is_opened {
|
|
return Ok(());
|
|
}
|
|
let latest_children = self.get_children()?;
|
|
let filtered = std::mem::take(&mut self.children)
|
|
.into_iter()
|
|
// Remove children that does not exists in latest_children
|
|
.filter(|tree| {
|
|
latest_children
|
|
.iter()
|
|
.any(|child| tree.item.name().eq(&child.item.name()))
|
|
})
|
|
.map(|mut tree| {
|
|
tree.refresh()?;
|
|
Ok(tree)
|
|
})
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
// Add new children
|
|
let new_nodes = latest_children
|
|
.into_iter()
|
|
.filter(|child| {
|
|
!filtered
|
|
.iter()
|
|
.any(|child_| child.item.name().eq(&child_.item.name()))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
self.children = filtered.into_iter().chain(new_nodes).collect();
|
|
|
|
self.sort();
|
|
|
|
self.regenerate_index();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn get_children(&self) -> Result<Vec<Tree<T>>> {
|
|
Ok(vec_to_tree(self.item.get_children()?))
|
|
}
|
|
|
|
fn sort(&mut self) {
|
|
self.children
|
|
.sort_by(|a, b| tree_item_cmp(&a.item, &b.item))
|
|
}
|
|
}
|
|
|
|
impl<T> Tree<T> {
|
|
pub fn new(item: T, children: Vec<Tree<T>>) -> Self {
|
|
let is_opened = !children.is_empty();
|
|
Self {
|
|
item,
|
|
index: 0,
|
|
parent_index: None,
|
|
children: index_elems(0, children),
|
|
is_opened,
|
|
}
|
|
}
|
|
|
|
fn iter(&self) -> TreeIter<T> {
|
|
TreeIter {
|
|
tree: self,
|
|
current_index_forward: 0,
|
|
current_index_reverse: (self.len() - 1) as isize,
|
|
}
|
|
}
|
|
|
|
/// Find an element in the tree with given `predicate`.
|
|
/// `start_index` is inclusive if direction is `Forward`.
|
|
/// `start_index` is exclusive if direction is `Backward`.
|
|
fn find<F>(&self, start_index: usize, direction: Direction, predicate: F) -> Option<usize>
|
|
where
|
|
F: Clone + FnMut(&Tree<T>) -> bool,
|
|
{
|
|
match direction {
|
|
Direction::Forward => match self
|
|
.iter()
|
|
.skip(start_index)
|
|
.position(predicate.clone())
|
|
.map(|index| index + start_index)
|
|
{
|
|
Some(index) => Some(index),
|
|
None => self.iter().position(predicate),
|
|
},
|
|
|
|
Direction::Backward => match self.iter().take(start_index).rposition(predicate.clone())
|
|
{
|
|
Some(index) => Some(index),
|
|
None => self.iter().rposition(predicate),
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn item(&self) -> &T {
|
|
&self.item
|
|
}
|
|
|
|
fn get(&self, index: usize) -> Option<&Tree<T>> {
|
|
if self.index == index {
|
|
Some(self)
|
|
} else {
|
|
self.children.iter().find_map(|elem| elem.get(index))
|
|
}
|
|
}
|
|
|
|
fn get_mut(&mut self, index: usize) -> Option<&mut Tree<T>> {
|
|
if self.index == index {
|
|
Some(self)
|
|
} else {
|
|
self.children
|
|
.iter_mut()
|
|
.find_map(|elem| elem.get_mut(index))
|
|
}
|
|
}
|
|
|
|
fn len(&self) -> usize {
|
|
(1_usize).saturating_add(self.children.iter().map(|elem| elem.len()).sum())
|
|
}
|
|
|
|
fn regenerate_index(&mut self) {
|
|
let items = std::mem::take(&mut self.children);
|
|
self.children = index_elems(0, items);
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct SavedView {
|
|
selected: usize,
|
|
winline: usize,
|
|
}
|
|
|
|
pub struct TreeView<T: TreeViewItem> {
|
|
tree: Tree<T>,
|
|
|
|
search_prompt: Option<(Direction, Prompt)>,
|
|
|
|
search_str: String,
|
|
|
|
/// Selected item idex
|
|
selected: usize,
|
|
|
|
backward_jumps: Vec<usize>,
|
|
forward_jumps: Vec<usize>,
|
|
|
|
saved_view: Option<SavedView>,
|
|
|
|
/// For implementing vertical scroll
|
|
winline: usize,
|
|
|
|
/// For implementing horizontal scoll
|
|
column: usize,
|
|
|
|
/// For implementing horizontal scoll
|
|
max_len: usize,
|
|
count: usize,
|
|
tree_symbol_style: String,
|
|
pub icons: TreeIcons,
|
|
|
|
#[allow(clippy::type_complexity)]
|
|
pre_render: Option<Box<dyn Fn(&mut Self, Rect) + 'static>>,
|
|
|
|
#[allow(clippy::type_complexity)]
|
|
on_opened_fn: Option<Box<dyn FnMut(&mut T, &mut Context, &mut T::Params) -> TreeOp + 'static>>,
|
|
|
|
#[allow(clippy::type_complexity)]
|
|
on_folded_fn: Option<Box<dyn FnMut(&mut T, &mut Context, &mut T::Params) + 'static>>,
|
|
|
|
#[allow(clippy::type_complexity)]
|
|
on_next_key: Option<Box<dyn FnMut(&mut Context, &mut Self, &KeyEvent) -> Result<()>>>,
|
|
}
|
|
|
|
impl<T: TreeViewItem> TreeView<T> {
|
|
pub fn build_tree(root: T) -> Result<Self> {
|
|
let children = root.get_children()?;
|
|
let items = vec_to_tree(children);
|
|
Ok(Self {
|
|
tree: Tree::new(root, items),
|
|
selected: 0,
|
|
backward_jumps: vec![],
|
|
forward_jumps: vec![],
|
|
saved_view: None,
|
|
winline: 0,
|
|
column: 0,
|
|
max_len: 0,
|
|
count: 0,
|
|
tree_symbol_style: "ui.text".into(),
|
|
pre_render: None,
|
|
on_opened_fn: None,
|
|
on_folded_fn: None,
|
|
on_next_key: None,
|
|
search_prompt: None,
|
|
search_str: "".into(),
|
|
icons: TreeIcons::default(),
|
|
})
|
|
}
|
|
|
|
pub fn with_enter_fn<F>(mut self, f: F) -> Self
|
|
where
|
|
F: FnMut(&mut T, &mut Context, &mut T::Params) -> TreeOp + 'static,
|
|
{
|
|
self.on_opened_fn = Some(Box::new(f));
|
|
self
|
|
}
|
|
|
|
pub fn with_folded_fn<F>(mut self, f: F) -> Self
|
|
where
|
|
F: FnMut(&mut T, &mut Context, &mut T::Params) + 'static,
|
|
{
|
|
self.on_folded_fn = Some(Box::new(f));
|
|
self
|
|
}
|
|
|
|
pub fn tree_symbol_style(mut self, style: String) -> Self {
|
|
self.tree_symbol_style = style;
|
|
self
|
|
}
|
|
|
|
pub fn icons(mut self, icons: TreeIcons) -> Self {
|
|
self.icons = icons;
|
|
self
|
|
}
|
|
|
|
/// Reveal item in the tree based on the given `segments`.
|
|
///
|
|
/// The name of the root should be excluded.
|
|
///
|
|
/// Example `segments`:
|
|
///
|
|
/// vec!["helix-term", "src", "ui", "tree.rs"]
|
|
///
|
|
pub fn reveal_item(&mut self, segments: Vec<String>) -> Result<()> {
|
|
self.refresh()?;
|
|
|
|
// Expand the tree
|
|
let root = self.tree.item.name();
|
|
segments.iter().fold(
|
|
Ok(&mut self.tree),
|
|
|current_tree, segment| match current_tree {
|
|
Err(err) => Err(err),
|
|
Ok(current_tree) => {
|
|
match current_tree
|
|
.children
|
|
.iter_mut()
|
|
.find(|tree| tree.item.name().eq(segment))
|
|
{
|
|
Some(tree) => {
|
|
if !tree.is_opened {
|
|
tree.open()?;
|
|
}
|
|
Ok(tree)
|
|
}
|
|
None => Err(anyhow::anyhow!(format!(
|
|
"Unable to find path: '{}'. current_segment = '{segment}'. current_root = '{root}'",
|
|
segments.join("/"),
|
|
))),
|
|
}
|
|
}
|
|
},
|
|
)?;
|
|
|
|
// Locate the item
|
|
self.regenerate_index();
|
|
self.set_selected(
|
|
segments
|
|
.iter()
|
|
.fold(&self.tree, |tree, segment| {
|
|
tree.children
|
|
.iter()
|
|
.find(|tree| tree.item.name().eq(segment))
|
|
.expect("Should be unreachable")
|
|
})
|
|
.index,
|
|
);
|
|
|
|
self.align_view_center();
|
|
Ok(())
|
|
}
|
|
|
|
fn align_view_center(&mut self) {
|
|
self.pre_render = Some(Box::new(|tree, area| {
|
|
tree.winline = area.height as usize / 2
|
|
}))
|
|
}
|
|
|
|
fn align_view_top(&mut self) {
|
|
self.winline = 0
|
|
}
|
|
|
|
fn align_view_bottom(&mut self) {
|
|
self.pre_render = Some(Box::new(|tree, area| tree.winline = area.height as usize))
|
|
}
|
|
|
|
fn regenerate_index(&mut self) {
|
|
self.tree.regenerate_index();
|
|
}
|
|
|
|
fn move_to_parent(&mut self) -> Result<()> {
|
|
if let Some(parent) = self.current_parent()? {
|
|
let index = parent.index;
|
|
self.set_selected(index)
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn move_to_children(&mut self) -> Result<()> {
|
|
let current = self.current_mut()?;
|
|
if current.is_opened {
|
|
self.set_selected(self.selected + 1);
|
|
Ok(())
|
|
} else {
|
|
current.open()?;
|
|
if !current.children.is_empty() {
|
|
self.set_selected(self.selected + 1);
|
|
self.regenerate_index();
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub fn refresh(&mut self) -> Result<()> {
|
|
self.tree.refresh()?;
|
|
self.set_selected(self.selected);
|
|
Ok(())
|
|
}
|
|
|
|
fn move_to_first_line(&mut self) {
|
|
self.move_up(usize::MAX / 2)
|
|
}
|
|
|
|
fn move_to_last_line(&mut self) {
|
|
self.move_down(usize::MAX / 2)
|
|
}
|
|
|
|
fn move_leftmost(&mut self) {
|
|
self.move_left(usize::MAX / 2);
|
|
}
|
|
|
|
fn move_rightmost(&mut self) {
|
|
self.move_right(usize::MAX / 2)
|
|
}
|
|
|
|
fn restore_saved_view(&mut self) -> Result<()> {
|
|
if let Some(saved_view) = self.saved_view.take() {
|
|
self.selected = saved_view.selected;
|
|
self.winline = saved_view.winline;
|
|
self.refresh()
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub fn prompt(&self) -> Option<&Prompt> {
|
|
if let Some((_, prompt)) = self.search_prompt.as_ref() {
|
|
Some(prompt)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn tree_view_help() -> Vec<(&'static str, &'static str)> {
|
|
vec![
|
|
("o, Enter", "Open/Close"),
|
|
("j, down, C-n", "Down"),
|
|
("k, up, C-p", "Up"),
|
|
("h, left", "Go to parent"),
|
|
("l, right", "Expand"),
|
|
("J", "Go to next sibling"),
|
|
("K", "Go to previous sibling"),
|
|
("H", "Go to first child"),
|
|
("L", "Go to last child"),
|
|
("R", "Refresh"),
|
|
("/", "Search"),
|
|
("n", "Go to next search match"),
|
|
("N", "Go to previous search match"),
|
|
("gh, Home", "Scroll to the leftmost"),
|
|
("gl, End", "Scroll to the rightmost"),
|
|
("C-o", "Jump backward"),
|
|
("C-i, Tab", "Jump forward"),
|
|
("C-d", "Half page down"),
|
|
("C-u", "Half page up"),
|
|
("PageDown", "Full page down"),
|
|
("PageUp", "Full page up"),
|
|
("zt", "Align view top"),
|
|
("zz", "Align view center"),
|
|
("zb", "Align view bottom"),
|
|
("gg", "Go to first line"),
|
|
("ge", "Go to last line"),
|
|
]
|
|
}
|
|
|
|
impl<T: TreeViewItem> TreeView<T> {
|
|
pub fn on_enter(
|
|
&mut self,
|
|
cx: &mut Context,
|
|
params: &mut T::Params,
|
|
selected_index: usize,
|
|
) -> Result<()> {
|
|
let selected_item = self.get_mut(selected_index)?;
|
|
if selected_item.is_opened {
|
|
selected_item.close();
|
|
self.regenerate_index();
|
|
return Ok(());
|
|
}
|
|
|
|
if let Some(mut on_open_fn) = self.on_opened_fn.take() {
|
|
let mut f = || -> Result<()> {
|
|
let current = self.current_mut()?;
|
|
match on_open_fn(&mut current.item, cx, params) {
|
|
TreeOp::GetChildsAndInsert => {
|
|
if let Err(err) = current.open() {
|
|
cx.editor.set_error(format!("{err}"))
|
|
}
|
|
}
|
|
TreeOp::Noop => {}
|
|
};
|
|
Ok(())
|
|
};
|
|
f()?;
|
|
self.regenerate_index();
|
|
self.on_opened_fn = Some(on_open_fn);
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
fn set_search_str(&mut self, s: String) {
|
|
self.search_str = s;
|
|
self.saved_view = None;
|
|
}
|
|
|
|
fn saved_view(&self) -> SavedView {
|
|
self.saved_view.clone().unwrap_or(SavedView {
|
|
selected: self.selected,
|
|
winline: self.winline,
|
|
})
|
|
}
|
|
|
|
fn search_next(&mut self, s: &str) {
|
|
let saved_view = self.saved_view();
|
|
let skip = std::cmp::max(2, saved_view.selected + 1);
|
|
self.set_selected(
|
|
self.tree
|
|
.find(skip, Direction::Forward, |e| e.item.filter(s))
|
|
.unwrap_or(saved_view.selected),
|
|
);
|
|
}
|
|
|
|
fn search_previous(&mut self, s: &str) {
|
|
let saved_view = self.saved_view();
|
|
let take = saved_view.selected;
|
|
self.set_selected(
|
|
self.tree
|
|
.find(take, Direction::Backward, |e| e.item.filter(s))
|
|
.unwrap_or(saved_view.selected),
|
|
);
|
|
}
|
|
|
|
fn move_to_next_search_match(&mut self) {
|
|
self.search_next(&self.search_str.clone())
|
|
}
|
|
|
|
fn move_to_previous_next_match(&mut self) {
|
|
self.search_previous(&self.search_str.clone())
|
|
}
|
|
|
|
pub fn move_down(&mut self, rows: usize) {
|
|
self.set_selected(self.selected.saturating_add(rows))
|
|
}
|
|
|
|
fn set_selected(&mut self, selected: usize) {
|
|
let previous_selected = self.selected;
|
|
self.set_selected_without_history(selected);
|
|
if previous_selected.abs_diff(selected) > 1 {
|
|
self.backward_jumps.push(previous_selected)
|
|
}
|
|
}
|
|
|
|
fn set_selected_without_history(&mut self, selected: usize) {
|
|
let selected = selected.clamp(0, self.tree.len().saturating_sub(1));
|
|
if selected > self.selected {
|
|
// Move down
|
|
self.winline = selected.min(
|
|
self.winline
|
|
.saturating_add(selected.saturating_sub(self.selected)),
|
|
);
|
|
} else {
|
|
// Move up
|
|
self.winline = selected.min(
|
|
self.winline
|
|
.saturating_sub(self.selected.saturating_sub(selected)),
|
|
);
|
|
}
|
|
self.selected = selected
|
|
}
|
|
|
|
fn jump_backward(&mut self) {
|
|
if let Some(index) = self.backward_jumps.pop() {
|
|
self.forward_jumps.push(self.selected);
|
|
self.set_selected_without_history(index);
|
|
}
|
|
}
|
|
|
|
fn jump_forward(&mut self) {
|
|
if let Some(index) = self.forward_jumps.pop() {
|
|
self.set_selected(index)
|
|
}
|
|
}
|
|
|
|
pub fn move_up(&mut self, rows: usize) {
|
|
self.set_selected(self.selected.saturating_sub(rows))
|
|
}
|
|
|
|
fn move_to_next_sibling(&mut self) -> Result<()> {
|
|
if let Some(parent) = self.current_parent()? {
|
|
if let Some(local_index) = parent
|
|
.children
|
|
.iter()
|
|
.position(|child| child.index == self.selected)
|
|
{
|
|
if let Some(next_sibling) = parent.children.get(local_index.saturating_add(1)) {
|
|
self.set_selected(next_sibling.index)
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn move_to_previous_sibling(&mut self) -> Result<()> {
|
|
if let Some(parent) = self.current_parent()? {
|
|
if let Some(local_index) = parent
|
|
.children
|
|
.iter()
|
|
.position(|child| child.index == self.selected)
|
|
{
|
|
if let Some(next_sibling) = parent.children.get(local_index.saturating_sub(1)) {
|
|
self.set_selected(next_sibling.index)
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn move_to_last_sibling(&mut self) -> Result<()> {
|
|
if let Some(parent) = self.current_parent()? {
|
|
if let Some(last) = parent.children.last() {
|
|
self.set_selected(last.index)
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn move_to_first_sibling(&mut self) -> Result<()> {
|
|
if let Some(parent) = self.current_parent()? {
|
|
if let Some(last) = parent.children.first() {
|
|
self.set_selected(last.index)
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn move_left(&mut self, cols: usize) {
|
|
self.column = self.column.saturating_sub(cols);
|
|
}
|
|
|
|
fn move_right(&mut self, cols: usize) {
|
|
self.pre_render = Some(Box::new(move |tree, area| {
|
|
let max_scroll = tree
|
|
.max_len
|
|
.saturating_sub(area.width as usize)
|
|
.saturating_add(1);
|
|
tree.column = max_scroll.min(tree.column + cols);
|
|
}));
|
|
}
|
|
|
|
fn move_down_half_page(&mut self) {
|
|
self.pre_render = Some(Box::new(|tree, area| {
|
|
tree.move_down((area.height / 2) as usize);
|
|
}));
|
|
}
|
|
|
|
fn move_up_half_page(&mut self) {
|
|
self.pre_render = Some(Box::new(|tree, area| {
|
|
tree.move_up((area.height / 2) as usize);
|
|
}));
|
|
}
|
|
|
|
fn move_down_page(&mut self) {
|
|
self.pre_render = Some(Box::new(|tree, area| {
|
|
tree.move_down((area.height) as usize);
|
|
}));
|
|
}
|
|
|
|
fn move_up_page(&mut self) {
|
|
self.pre_render = Some(Box::new(|tree, area| {
|
|
tree.move_up((area.height) as usize);
|
|
}));
|
|
}
|
|
|
|
fn save_view(&mut self) {
|
|
self.saved_view = Some(SavedView {
|
|
selected: self.selected,
|
|
winline: self.winline,
|
|
})
|
|
}
|
|
|
|
fn get(&self, index: usize) -> Result<&Tree<T>> {
|
|
self.tree.get(index).ok_or_else(|| {
|
|
anyhow::anyhow!("Programming error: TreeView.get: index {index} is out of bound")
|
|
})
|
|
}
|
|
|
|
fn get_mut(&mut self, index: usize) -> Result<&mut Tree<T>> {
|
|
self.tree.get_mut(index).ok_or_else(|| {
|
|
anyhow::anyhow!("Programming error: TreeView.get_mut: index {index} is out of bound")
|
|
})
|
|
}
|
|
|
|
pub fn current(&self) -> Result<&Tree<T>> {
|
|
self.get(self.selected)
|
|
}
|
|
|
|
pub fn current_mut(&mut self) -> Result<&mut Tree<T>> {
|
|
self.get_mut(self.selected)
|
|
}
|
|
|
|
fn current_parent(&self) -> Result<Option<&Tree<T>>> {
|
|
if let Some(parent_index) = self.current()?.parent_index {
|
|
Ok(Some(self.get(parent_index)?))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
pub fn current_item(&self) -> Result<&T> {
|
|
Ok(&self.current()?.item)
|
|
}
|
|
|
|
pub fn winline(&self) -> usize {
|
|
self.winline
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct RenderedLine {
|
|
indent: String,
|
|
content: String,
|
|
selected: bool,
|
|
is_ancestor_of_current_item: bool,
|
|
icon: Icon,
|
|
}
|
|
struct RenderTreeParams<'a, T> {
|
|
tree: &'a Tree<T>,
|
|
prefix: &'a String,
|
|
level: usize,
|
|
selected: usize,
|
|
icons: &'a TreeIcons,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct TreeIcons {
|
|
pub tree_closed: Icon,
|
|
pub tree_opened: Icon,
|
|
pub item: Icon,
|
|
pub icon_fn: Option<Arc<dyn Fn(&str) -> Icon>>,
|
|
}
|
|
|
|
impl Default for TreeIcons {
|
|
fn default() -> Self {
|
|
Self {
|
|
tree_closed: Icon::unstyled('⏵'),
|
|
tree_opened: Icon::unstyled('⏷'),
|
|
item: Icon::unstyled(' '),
|
|
icon_fn: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn render_tree<T: TreeViewItem>(
|
|
RenderTreeParams {
|
|
tree,
|
|
prefix,
|
|
level,
|
|
selected,
|
|
icons,
|
|
}: RenderTreeParams<T>,
|
|
) -> Vec<RenderedLine> {
|
|
let name = tree.item.name();
|
|
|
|
let icon = if tree.item().is_parent() {
|
|
if tree.is_opened {
|
|
icons.tree_opened.to_owned()
|
|
} else {
|
|
icons.tree_closed.to_owned()
|
|
}
|
|
} else {
|
|
if let Some(icon_fn) = icons.icon_fn.as_ref() {
|
|
icon_fn(&name)
|
|
} else {
|
|
icons.item.to_owned()
|
|
}
|
|
};
|
|
|
|
let indent = if level > 0 {
|
|
format!("{} ", prefix)
|
|
} else {
|
|
String::new()
|
|
};
|
|
let head = RenderedLine {
|
|
indent,
|
|
selected: selected == tree.index,
|
|
is_ancestor_of_current_item: selected != tree.index && tree.get(selected).is_some(),
|
|
content: name,
|
|
icon,
|
|
};
|
|
let prefix = format!("{}{}", prefix, if level == 0 { "" } else { " " });
|
|
vec![head]
|
|
.into_iter()
|
|
.chain(tree.children.iter().flat_map(|elem| {
|
|
render_tree(RenderTreeParams {
|
|
tree: elem,
|
|
prefix: &prefix,
|
|
level: level + 1,
|
|
selected,
|
|
icons,
|
|
})
|
|
}))
|
|
.collect()
|
|
}
|
|
|
|
impl<T: TreeViewItem + Clone> TreeView<T> {
|
|
pub fn render(
|
|
&mut self,
|
|
area: Rect,
|
|
prompt_area: Rect,
|
|
surface: &mut Surface,
|
|
cx: &mut Context,
|
|
) {
|
|
let style = cx.editor.theme.get(&self.tree_symbol_style);
|
|
if let Some((_, prompt)) = self.search_prompt.as_mut() {
|
|
prompt.render_prompt(prompt_area, surface, cx)
|
|
}
|
|
|
|
let ancestor_style = {
|
|
let style = cx.editor.theme.get("ui.selection");
|
|
let fg = cx.editor.theme.get("ui.text").fg;
|
|
match (style.fg, fg) {
|
|
(None, Some(fg)) => style.fg(fg),
|
|
_ => style,
|
|
}
|
|
};
|
|
|
|
let iter = self.render_lines(area).into_iter().enumerate();
|
|
|
|
for (index, line) in iter {
|
|
let area = Rect::new(area.x, area.y.saturating_add(index as u16), area.width, 1);
|
|
let indent_len = line.indent.chars().count() as u16;
|
|
surface.set_stringn(
|
|
area.x,
|
|
area.y,
|
|
line.indent.clone(),
|
|
indent_len as usize,
|
|
style,
|
|
);
|
|
|
|
let x = area.x.saturating_add(indent_len);
|
|
surface.set_stringn(
|
|
x,
|
|
area.y,
|
|
line.icon.icon_char.to_string(),
|
|
2,
|
|
line.icon.style.map(|s| s.into()).unwrap_or(style),
|
|
);
|
|
let style = if line.selected {
|
|
style.add_modifier(Modifier::REVERSED)
|
|
} else {
|
|
style
|
|
};
|
|
let x = x.saturating_add(2);
|
|
surface.set_stringn(
|
|
x,
|
|
area.y,
|
|
line.content.clone(),
|
|
area.width
|
|
.saturating_sub(indent_len)
|
|
.saturating_sub(3)
|
|
.into(),
|
|
if line.is_ancestor_of_current_item {
|
|
ancestor_style
|
|
} else {
|
|
style
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn render_to_string(&mut self, area: Rect) -> String {
|
|
let lines = self.render_lines(area);
|
|
lines
|
|
.into_iter()
|
|
.map(|line| {
|
|
let name = if line.selected {
|
|
format!("({})", line.content)
|
|
} else if line.is_ancestor_of_current_item {
|
|
format!("[{}]", line.content)
|
|
} else {
|
|
line.content
|
|
};
|
|
format!("{}{}", line.indent, name)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
|
|
fn render_lines(&mut self, area: Rect) -> Vec<RenderedLine> {
|
|
if let Some(pre_render) = self.pre_render.take() {
|
|
pre_render(self, area);
|
|
}
|
|
|
|
self.winline = self.winline.min(area.height.saturating_sub(1) as usize);
|
|
let skip = self.selected.saturating_sub(self.winline);
|
|
let params = RenderTreeParams {
|
|
tree: &self.tree,
|
|
prefix: &"".to_string(),
|
|
level: 0,
|
|
selected: self.selected,
|
|
icons: &self.icons,
|
|
};
|
|
|
|
let lines = render_tree(params);
|
|
|
|
self.max_len = lines
|
|
.iter()
|
|
.map(|line| {
|
|
line.indent
|
|
.chars()
|
|
.count()
|
|
.saturating_add(line.content.chars().count())
|
|
})
|
|
.max()
|
|
.unwrap_or(0);
|
|
|
|
let max_width = area.width as usize;
|
|
|
|
let take = area.height as usize;
|
|
|
|
struct RetainAncestorResult {
|
|
skipped_ancestors: Vec<RenderedLine>,
|
|
remaining_lines: Vec<RenderedLine>,
|
|
}
|
|
fn retain_ancestors(lines: Vec<RenderedLine>, skip: usize) -> RetainAncestorResult {
|
|
if skip == 0 {
|
|
return RetainAncestorResult {
|
|
skipped_ancestors: vec![],
|
|
remaining_lines: lines,
|
|
};
|
|
}
|
|
if let Some(line) = lines.get(0) {
|
|
if line.selected {
|
|
return RetainAncestorResult {
|
|
skipped_ancestors: vec![],
|
|
remaining_lines: lines,
|
|
};
|
|
}
|
|
}
|
|
|
|
let selected_index = lines.iter().position(|line| line.selected);
|
|
let skip = match selected_index {
|
|
None => skip,
|
|
Some(selected_index) => skip.min(selected_index),
|
|
};
|
|
let (skipped, remaining) = lines.split_at(skip.min(lines.len().saturating_sub(1)));
|
|
|
|
let skipped_ancestors = skipped
|
|
.iter()
|
|
.cloned()
|
|
.filter(|line| line.is_ancestor_of_current_item)
|
|
.collect::<Vec<_>>();
|
|
|
|
let result = retain_ancestors(remaining.to_vec(), skipped_ancestors.len());
|
|
RetainAncestorResult {
|
|
skipped_ancestors: skipped_ancestors
|
|
.into_iter()
|
|
.chain(result.skipped_ancestors.into_iter())
|
|
.collect(),
|
|
remaining_lines: result.remaining_lines,
|
|
}
|
|
}
|
|
|
|
let RetainAncestorResult {
|
|
skipped_ancestors,
|
|
remaining_lines,
|
|
} = retain_ancestors(lines, skip);
|
|
|
|
let max_ancestors_len = take.saturating_sub(1);
|
|
|
|
// Skip furthest ancestors
|
|
let skipped_ancestors = skipped_ancestors
|
|
.into_iter()
|
|
.rev()
|
|
.take(max_ancestors_len)
|
|
.rev()
|
|
.collect::<Vec<_>>();
|
|
|
|
let skipped_ancestors_len = skipped_ancestors.len();
|
|
|
|
skipped_ancestors
|
|
.into_iter()
|
|
.chain(
|
|
remaining_lines
|
|
.into_iter()
|
|
.take(take.saturating_sub(skipped_ancestors_len)),
|
|
)
|
|
// Horizontal scroll
|
|
.map(|line| {
|
|
let skip = self.column;
|
|
let indent_len = line.indent.chars().count();
|
|
RenderedLine {
|
|
indent: if line.indent.is_empty() {
|
|
"".to_string()
|
|
} else {
|
|
line.indent
|
|
.chars()
|
|
.skip(skip)
|
|
.take(max_width)
|
|
.collect::<String>()
|
|
},
|
|
content: line
|
|
.content
|
|
.chars()
|
|
.skip(skip.saturating_sub(indent_len))
|
|
.take((max_width.saturating_sub(indent_len)).clamp(0, line.content.len()))
|
|
.collect::<String>(),
|
|
..line
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn handle_events(
|
|
&mut self,
|
|
events: &str,
|
|
cx: &mut Context,
|
|
params: &mut T::Params,
|
|
) -> Result<()> {
|
|
use helix_view::input::parse_macro;
|
|
|
|
for event in parse_macro(events)? {
|
|
self.handle_event(&Event::Key(event), cx, params);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn handle_event(
|
|
&mut self,
|
|
event: &Event,
|
|
cx: &mut Context,
|
|
params: &mut T::Params,
|
|
) -> EventResult {
|
|
let key_event = match event {
|
|
Event::Key(event) => event,
|
|
Event::Resize(..) => return EventResult::Consumed(None),
|
|
_ => return EventResult::Ignored(None),
|
|
};
|
|
(|| -> Result<EventResult> {
|
|
if let Some(mut on_next_key) = self.on_next_key.take() {
|
|
on_next_key(cx, self, key_event)?;
|
|
return Ok(EventResult::Consumed(None));
|
|
}
|
|
|
|
if let EventResult::Consumed(c) = self.handle_search_event(key_event, cx) {
|
|
return Ok(EventResult::Consumed(c));
|
|
}
|
|
|
|
let count = std::mem::replace(&mut self.count, 0);
|
|
|
|
match key_event {
|
|
key!(i @ '0'..='9') => {
|
|
self.count = i.to_digit(10).unwrap_or(0) as usize + count * 10
|
|
}
|
|
shift!('J') => self.move_to_next_sibling()?,
|
|
shift!('K') => self.move_to_previous_sibling()?,
|
|
shift!('H') => self.move_to_first_sibling()?,
|
|
shift!('L') => self.move_to_last_sibling()?,
|
|
key!('j') | key!(Down) | ctrl!('n') => self.move_down(1.max(count)),
|
|
key!('k') | key!(Up) | ctrl!('p') => self.move_up(1.max(count)),
|
|
key!('h') | key!(Left) => self.move_to_parent()?,
|
|
key!('l') | key!(Right) => self.move_to_children()?,
|
|
key!(Enter) | key!('o') => self.on_enter(cx, params, self.selected)?,
|
|
ctrl!('d') => self.move_down_half_page(),
|
|
ctrl!('u') => self.move_up_half_page(),
|
|
key!('z') => {
|
|
self.on_next_key = Some(Box::new(|_, tree, event| {
|
|
match event {
|
|
key!('z') => tree.align_view_center(),
|
|
key!('t') => tree.align_view_top(),
|
|
key!('b') => tree.align_view_bottom(),
|
|
_ => {}
|
|
};
|
|
Ok(())
|
|
}));
|
|
}
|
|
key!('g') => {
|
|
self.on_next_key = Some(Box::new(|_, tree, event| {
|
|
match event {
|
|
key!('g') => tree.move_to_first_line(),
|
|
key!('e') => tree.move_to_last_line(),
|
|
key!('h') => tree.move_leftmost(),
|
|
key!('l') => tree.move_rightmost(),
|
|
_ => {}
|
|
};
|
|
Ok(())
|
|
}));
|
|
}
|
|
key!('/') => self.new_search_prompt(Direction::Forward),
|
|
key!('n') => self.move_to_next_search_match(),
|
|
shift!('N') => self.move_to_previous_next_match(),
|
|
key!(PageDown) => self.move_down_page(),
|
|
key!(PageUp) => self.move_up_page(),
|
|
shift!('R') => {
|
|
if let Err(error) = self.refresh() {
|
|
cx.editor.set_error(error.to_string())
|
|
}
|
|
}
|
|
key!(Home) => self.move_leftmost(),
|
|
key!(End) => self.move_rightmost(),
|
|
ctrl!('o') => self.jump_backward(),
|
|
ctrl!('i') | key!(Tab) => self.jump_forward(),
|
|
_ => return Ok(EventResult::Ignored(None)),
|
|
};
|
|
Ok(EventResult::Consumed(None))
|
|
})()
|
|
.unwrap_or_else(|err| {
|
|
cx.editor.set_error(format!("{err}"));
|
|
EventResult::Consumed(None)
|
|
})
|
|
}
|
|
|
|
fn handle_search_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult {
|
|
if let Some((direction, mut prompt)) = self.search_prompt.take() {
|
|
match event {
|
|
key!(Enter) => {
|
|
self.set_search_str(prompt.line().clone());
|
|
EventResult::Consumed(None)
|
|
}
|
|
key!(Esc) => {
|
|
if let Err(err) = self.restore_saved_view() {
|
|
cx.editor.set_error(format!("{err}"))
|
|
}
|
|
EventResult::Consumed(None)
|
|
}
|
|
_ => {
|
|
let event = prompt.handle_event(&Event::Key(*event), cx);
|
|
let line = prompt.line();
|
|
match direction {
|
|
Direction::Forward => {
|
|
self.search_next(line);
|
|
}
|
|
Direction::Backward => self.search_previous(line),
|
|
}
|
|
self.search_prompt = Some((direction, prompt));
|
|
event
|
|
}
|
|
}
|
|
} else {
|
|
EventResult::Ignored(None)
|
|
}
|
|
}
|
|
|
|
fn new_search_prompt(&mut self, direction: Direction) {
|
|
self.save_view();
|
|
self.search_prompt = Some((
|
|
direction,
|
|
Prompt::new("search: ".into(), None, ui::completers::none, |_, _, _| {}),
|
|
))
|
|
}
|
|
|
|
pub fn prompting(&self) -> bool {
|
|
self.search_prompt.is_some() || self.on_next_key.is_some()
|
|
}
|
|
}
|
|
|
|
/// Recalculate the index of each item of a tree.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```txt
|
|
/// foo (0)
|
|
/// bar (1)
|
|
/// spam (2)
|
|
/// jar (3)
|
|
/// yo (4)
|
|
/// ```
|
|
fn index_elems<T>(parent_index: usize, elems: Vec<Tree<T>>) -> Vec<Tree<T>> {
|
|
fn index_elems<T>(
|
|
current_index: usize,
|
|
elems: Vec<Tree<T>>,
|
|
parent_index: usize,
|
|
) -> (usize, Vec<Tree<T>>) {
|
|
elems
|
|
.into_iter()
|
|
.fold((current_index, vec![]), |(current_index, trees), elem| {
|
|
let index = current_index;
|
|
let item = elem.item;
|
|
let (current_index, folded) = index_elems(current_index + 1, elem.children, index);
|
|
let tree = Tree {
|
|
item,
|
|
children: folded,
|
|
index,
|
|
is_opened: elem.is_opened,
|
|
parent_index: Some(parent_index),
|
|
};
|
|
(
|
|
current_index,
|
|
trees.into_iter().chain(vec![tree].into_iter()).collect(),
|
|
)
|
|
})
|
|
}
|
|
index_elems(parent_index + 1, elems, parent_index).1
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test_tree_view {
|
|
|
|
use helix_view::graphics::Rect;
|
|
|
|
use crate::compositor::Context;
|
|
|
|
use super::{TreeView, TreeViewItem};
|
|
|
|
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
|
|
/// The children of DivisibleItem is the division of itself.
|
|
/// This is used to ease the creation of a dummy tree without having to specify so many things.
|
|
struct DivisibleItem<'a> {
|
|
name: &'a str,
|
|
}
|
|
|
|
fn item(name: &str) -> DivisibleItem {
|
|
DivisibleItem { name }
|
|
}
|
|
|
|
impl<'a> TreeViewItem for DivisibleItem<'a> {
|
|
type Params = ();
|
|
|
|
fn name(&self) -> String {
|
|
self.name.to_string()
|
|
}
|
|
|
|
fn is_parent(&self) -> bool {
|
|
self.name.len() > 2
|
|
}
|
|
|
|
fn get_children(&self) -> anyhow::Result<Vec<Self>> {
|
|
if self.name.eq("who_lives_in_a_pineapple_under_the_sea") {
|
|
Ok(vec![
|
|
item("gary_the_snail"),
|
|
item("krabby_patty"),
|
|
item("larry_the_lobster"),
|
|
item("patrick_star"),
|
|
item("sandy_cheeks"),
|
|
item("spongebob_squarepants"),
|
|
item("mrs_puff"),
|
|
item("king_neptune"),
|
|
item("karen"),
|
|
item("plankton"),
|
|
])
|
|
} else if self.is_parent() {
|
|
let (left, right) = self.name.split_at(self.name.len() / 2);
|
|
Ok(vec![item(left), item(right)])
|
|
} else {
|
|
Ok(vec![])
|
|
}
|
|
}
|
|
}
|
|
|
|
fn dummy_tree_view<'a>() -> TreeView<DivisibleItem<'a>> {
|
|
TreeView::build_tree(item("who_lives_in_a_pineapple_under_the_sea")).unwrap()
|
|
}
|
|
|
|
fn dummy_area() -> Rect {
|
|
Rect::new(0, 0, 50, 5)
|
|
}
|
|
|
|
fn render(view: &mut TreeView<DivisibleItem>) -> String {
|
|
view.render_to_string(dummy_area())
|
|
}
|
|
|
|
#[test]
|
|
fn test_init() {
|
|
let mut view = dummy_tree_view();
|
|
|
|
// Expect the items to be sorted
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(who_lives_in_a_pineapple_under_the_sea)
|
|
⏵ gary_the_snail
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_move_up_down() {
|
|
let mut view = dummy_tree_view();
|
|
view.move_down(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ (gary_the_snail)
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_down(3);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ gary_the_snail
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ (krabby_patty)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_down(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
⏵ (larry_the_lobster)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_up(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ (krabby_patty)
|
|
⏵ larry_the_lobster
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_up(3);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ (gary_the_snail)
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_up(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(who_lives_in_a_pineapple_under_the_sea)
|
|
⏵ gary_the_snail
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_first_line();
|
|
view.move_up(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(who_lives_in_a_pineapple_under_the_sea)
|
|
⏵ gary_the_snail
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_last_line();
|
|
view.move_down(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ patrick_star
|
|
⏵ plankton
|
|
⏵ sandy_cheeks
|
|
⏵ (spongebob_squarepants)
|
|
"
|
|
.trim()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_move_to_first_last_sibling() {
|
|
let mut view = dummy_tree_view();
|
|
view.move_to_children().unwrap();
|
|
view.move_to_children().unwrap();
|
|
view.move_to_parent().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏷ (gary_the_snail)
|
|
⏵ e_snail
|
|
⏵ gary_th
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_last_sibling().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ patrick_star
|
|
⏵ plankton
|
|
⏵ sandy_cheeks
|
|
⏵ (spongebob_squarepants)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_first_sibling().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏷ (gary_the_snail)
|
|
⏵ e_snail
|
|
⏵ gary_th
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_move_to_previous_next_sibling() {
|
|
let mut view = dummy_tree_view();
|
|
view.move_to_children().unwrap();
|
|
view.move_to_children().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏷ [gary_the_snail]
|
|
⏵ (e_snail)
|
|
⏵ gary_th
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_next_sibling().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏷ [gary_the_snail]
|
|
⏵ e_snail
|
|
⏵ (gary_th)
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_next_sibling().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏷ [gary_the_snail]
|
|
⏵ e_snail
|
|
⏵ (gary_th)
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_previous_sibling().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏷ [gary_the_snail]
|
|
⏵ (e_snail)
|
|
⏵ gary_th
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_previous_sibling().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏷ [gary_the_snail]
|
|
⏵ (e_snail)
|
|
⏵ gary_th
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_parent().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏷ (gary_the_snail)
|
|
⏵ e_snail
|
|
⏵ gary_th
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_next_sibling().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏷ gary_the_snail
|
|
⏵ e_snail
|
|
⏵ gary_th
|
|
⏵ (karen)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_previous_sibling().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏷ (gary_the_snail)
|
|
⏵ e_snail
|
|
⏵ gary_th
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_align_view() {
|
|
let mut view = dummy_tree_view();
|
|
view.move_down(5);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
⏵ (larry_the_lobster)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.align_view_center();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ krabby_patty
|
|
⏵ (larry_the_lobster)
|
|
⏵ mrs_puff
|
|
⏵ patrick_star
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.align_view_bottom();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
⏵ (larry_the_lobster)
|
|
"
|
|
.trim()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_move_to_first_last() {
|
|
let mut view = dummy_tree_view();
|
|
|
|
view.move_to_last_line();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ patrick_star
|
|
⏵ plankton
|
|
⏵ sandy_cheeks
|
|
⏵ (spongebob_squarepants)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_first_line();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(who_lives_in_a_pineapple_under_the_sea)
|
|
⏵ gary_the_snail
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_move_half() {
|
|
let mut view = dummy_tree_view();
|
|
view.move_down_half_page();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ gary_the_snail
|
|
⏵ (karen)
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_down_half_page();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ gary_the_snail
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ (krabby_patty)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_down_half_page();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
⏵ larry_the_lobster
|
|
⏵ (mrs_puff)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_up_half_page();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ king_neptune
|
|
⏵ (krabby_patty)
|
|
⏵ larry_the_lobster
|
|
⏵ mrs_puff
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_up_half_page();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ (karen)
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
⏵ larry_the_lobster
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_up_half_page();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(who_lives_in_a_pineapple_under_the_sea)
|
|
⏵ gary_the_snail
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn move_to_children_parent() {
|
|
let mut view = dummy_tree_view();
|
|
view.move_down(1);
|
|
view.move_to_children().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏷ [gary_the_snail]
|
|
⏵ (e_snail)
|
|
⏵ gary_th
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_down(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏷ [gary_the_snail]
|
|
⏵ e_snail
|
|
⏵ (gary_th)
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_parent().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏷ (gary_the_snail)
|
|
⏵ e_snail
|
|
⏵ gary_th
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_last_line();
|
|
view.move_to_parent().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(who_lives_in_a_pineapple_under_the_sea)
|
|
⏷ gary_the_snail
|
|
⏵ e_snail
|
|
⏵ gary_th
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_move_left_right() {
|
|
let mut view = dummy_tree_view();
|
|
|
|
fn render(view: &mut TreeView<DivisibleItem>) -> String {
|
|
view.render_to_string(dummy_area().with_width(20))
|
|
}
|
|
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(who_lives_in_a_pinea)
|
|
⏵ gary_the_snail
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_right(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(ho_lives_in_a_pineap)
|
|
gary_the_snail
|
|
karen
|
|
king_neptune
|
|
krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_right(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(o_lives_in_a_pineapp)
|
|
gary_the_snail
|
|
karen
|
|
king_neptune
|
|
krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_right(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(_lives_in_a_pineappl)
|
|
ary_the_snail
|
|
aren
|
|
ing_neptune
|
|
rabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_left(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(o_lives_in_a_pineapp)
|
|
gary_the_snail
|
|
karen
|
|
king_neptune
|
|
krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_leftmost();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(who_lives_in_a_pinea)
|
|
⏵ gary_the_snail
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_left(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(who_lives_in_a_pinea)
|
|
⏵ gary_the_snail
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_rightmost();
|
|
assert_eq!(render(&mut view), "(apple_under_the_sea)\n\n\n\n");
|
|
}
|
|
|
|
#[test]
|
|
fn test_move_to_parent_child() {
|
|
let mut view = dummy_tree_view();
|
|
|
|
view.move_to_children().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ (gary_the_snail)
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_children().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏷ [gary_the_snail]
|
|
⏵ (e_snail)
|
|
⏵ gary_th
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_down(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏷ [gary_the_snail]
|
|
⏵ e_snail
|
|
⏵ (gary_th)
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_parent().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏷ (gary_the_snail)
|
|
⏵ e_snail
|
|
⏵ gary_th
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_parent().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(who_lives_in_a_pineapple_under_the_sea)
|
|
⏷ gary_the_snail
|
|
⏵ e_snail
|
|
⏵ gary_th
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_parent().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(who_lives_in_a_pineapple_under_the_sea)
|
|
⏷ gary_the_snail
|
|
⏵ e_snail
|
|
⏵ gary_th
|
|
⏵ karen
|
|
"
|
|
.trim()
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn test_search_next() {
|
|
let mut view = dummy_tree_view();
|
|
|
|
view.search_next("pat");
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ gary_the_snail
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ (krabby_patty)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.search_next("larr");
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
⏵ (larry_the_lobster)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_last_line();
|
|
view.search_next("who_lives");
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(who_lives_in_a_pineapple_under_the_sea)
|
|
⏵ gary_the_snail
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_search_previous() {
|
|
let mut view = dummy_tree_view();
|
|
|
|
view.search_previous("larry");
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
⏵ (larry_the_lobster)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_last_line();
|
|
view.search_previous("krab");
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ (krabby_patty)
|
|
⏵ larry_the_lobster
|
|
"
|
|
.trim()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_move_to_next_search_match() {
|
|
let mut view = dummy_tree_view();
|
|
view.set_search_str("pat".to_string());
|
|
view.move_to_next_search_match();
|
|
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ gary_the_snail
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ (krabby_patty)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_next_search_match();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ krabby_patty
|
|
⏵ larry_the_lobster
|
|
⏵ mrs_puff
|
|
⏵ (patrick_star)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_next_search_match();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ (krabby_patty)
|
|
⏵ larry_the_lobster
|
|
⏵ mrs_puff
|
|
⏵ patrick_star
|
|
"
|
|
.trim()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_move_to_previous_search_match() {
|
|
let mut view = dummy_tree_view();
|
|
view.set_search_str("pat".to_string());
|
|
view.move_to_previous_next_match();
|
|
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ krabby_patty
|
|
⏵ larry_the_lobster
|
|
⏵ mrs_puff
|
|
⏵ (patrick_star)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_previous_next_match();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ (krabby_patty)
|
|
⏵ larry_the_lobster
|
|
⏵ mrs_puff
|
|
⏵ patrick_star
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.move_to_previous_next_match();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ krabby_patty
|
|
⏵ larry_the_lobster
|
|
⏵ mrs_puff
|
|
⏵ (patrick_star)
|
|
"
|
|
.trim()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_jump_backward_forward() {
|
|
let mut view = dummy_tree_view();
|
|
view.move_down_half_page();
|
|
render(&mut view);
|
|
|
|
view.move_down_half_page();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ gary_the_snail
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ (krabby_patty)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.jump_backward();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ gary_the_snail
|
|
⏵ (karen)
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.jump_backward();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(who_lives_in_a_pineapple_under_the_sea)
|
|
⏵ gary_the_snail
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.jump_forward();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ gary_the_snail
|
|
⏵ (karen)
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.jump_forward();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ gary_the_snail
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ (krabby_patty)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.jump_backward();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ gary_the_snail
|
|
⏵ (karen)
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
"
|
|
.trim()
|
|
);
|
|
}
|
|
|
|
mod static_tree {
|
|
use crate::ui::{TreeView, TreeViewItem};
|
|
|
|
use super::dummy_area;
|
|
|
|
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
|
|
/// This is used for test cases where the structure of the tree has to be known upfront
|
|
pub struct StaticItem<'a> {
|
|
pub name: &'a str,
|
|
pub children: Option<Vec<StaticItem<'a>>>,
|
|
}
|
|
|
|
pub fn parent<'a>(name: &'a str, children: Vec<StaticItem<'a>>) -> StaticItem<'a> {
|
|
StaticItem {
|
|
name,
|
|
children: Some(children),
|
|
}
|
|
}
|
|
|
|
pub fn child(name: &str) -> StaticItem {
|
|
StaticItem {
|
|
name,
|
|
children: None,
|
|
}
|
|
}
|
|
|
|
impl<'a> TreeViewItem for StaticItem<'a> {
|
|
type Params = ();
|
|
|
|
fn name(&self) -> String {
|
|
self.name.to_string()
|
|
}
|
|
|
|
fn is_parent(&self) -> bool {
|
|
self.children.is_some()
|
|
}
|
|
|
|
fn get_children(&self) -> anyhow::Result<Vec<Self>> {
|
|
match &self.children {
|
|
Some(children) => Ok(children.clone()),
|
|
None => Ok(vec![]),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn render(view: &mut TreeView<StaticItem<'_>>) -> String {
|
|
view.render_to_string(dummy_area().with_height(3))
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_sticky_ancestors() {
|
|
// The ancestors of the current item should always be visible
|
|
// However, if there's not enough space, the current item will take precedence,
|
|
// and the nearest ancestor has higher precedence than further ancestors
|
|
use static_tree::*;
|
|
|
|
let mut view = TreeView::build_tree(parent(
|
|
"root",
|
|
vec![
|
|
parent("a", vec![child("aa"), child("ab")]),
|
|
parent(
|
|
"b",
|
|
vec![parent(
|
|
"ba",
|
|
vec![parent("baa", vec![child("baaa"), child("baab")])],
|
|
)],
|
|
),
|
|
],
|
|
))
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
(root)
|
|
⏵ a
|
|
⏵ b
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
// 1. Move down to "a", and expand it
|
|
view.move_down(1);
|
|
view.move_to_children().unwrap();
|
|
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[root]
|
|
⏷ [a]
|
|
(aa)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
// 2. Move down by 1
|
|
view.move_down(1);
|
|
|
|
// 2a. Expect all ancestors (i.e. "root" and "a") are visible,
|
|
// and the cursor is at "ab"
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[root]
|
|
⏷ [a]
|
|
(ab)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
// 3. Move down by 1
|
|
view.move_down(1);
|
|
|
|
// 3a. Expect "a" is out of view, because it is no longer the ancestor of the current item
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[root]
|
|
ab
|
|
⏵ (b)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
// 4. Move to the children of "b", which is "ba"
|
|
view.move_to_children().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[root]
|
|
⏷ [b]
|
|
⏵ (ba)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
// 5. Move to the children of "ba", which is "baa"
|
|
view.move_to_children().unwrap();
|
|
|
|
// 5a. Expect the furthest ancestor "root" is out of view,
|
|
// because when there's no enough space, the nearest ancestor takes precedence
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
⏷ [b]
|
|
⏷ [ba]
|
|
⏵ (baa)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
// 5.1 Move to child
|
|
view.move_to_children().unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
⏷ [ba]
|
|
⏷ [baa]
|
|
(baaa)
|
|
"
|
|
.trim_matches('\n')
|
|
);
|
|
|
|
// 5.2 Move down
|
|
view.move_down(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
⏷ [ba]
|
|
⏷ [baa]
|
|
(baab)
|
|
"
|
|
.trim_matches('\n')
|
|
);
|
|
|
|
// 5.3 Move up
|
|
view.move_up(1);
|
|
assert_eq!(view.current_item().unwrap().name, "baaa");
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
⏷ [ba]
|
|
⏷ [baa]
|
|
(baaa)
|
|
"
|
|
.trim_matches('\n')
|
|
);
|
|
|
|
// 5.4 Move up
|
|
view.move_up(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
⏷ [b]
|
|
⏷ [ba]
|
|
⏷ (baa)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
// 6. Move up by one
|
|
view.move_up(1);
|
|
|
|
// 6a. Expect "root" is visible again, because now there's enough space to render all
|
|
// ancestors
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[root]
|
|
⏷ [b]
|
|
⏷ (ba)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
// 7. Move up by one
|
|
view.move_up(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[root]
|
|
⏷ (b)
|
|
⏷ ba
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
// 8. Move up by one
|
|
view.move_up(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[root]
|
|
⏷ [a]
|
|
(ab)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
// 9. Move up by one
|
|
view.move_up(1);
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[root]
|
|
⏷ [a]
|
|
(aa)
|
|
"
|
|
.trim()
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn test_search_prompt() {
|
|
let mut editor = Context::dummy_editor();
|
|
let mut jobs = Context::dummy_jobs();
|
|
let mut cx = Context::dummy(&mut jobs, &mut editor);
|
|
let mut view = dummy_tree_view();
|
|
|
|
view.handle_events("/an", &mut cx, &mut ()).unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ larry_the_lobster
|
|
⏵ mrs_puff
|
|
⏵ patrick_star
|
|
⏵ (plankton)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.handle_events("t<ret>", &mut cx, &mut ()).unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ patrick_star
|
|
⏵ plankton
|
|
⏵ sandy_cheeks
|
|
⏵ (spongebob_squarepants)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.handle_events("/larry", &mut cx, &mut ()).unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ karen
|
|
⏵ king_neptune
|
|
⏵ krabby_patty
|
|
⏵ (larry_the_lobster)
|
|
"
|
|
.trim()
|
|
);
|
|
|
|
view.handle_events("<esc>", &mut cx, &mut ()).unwrap();
|
|
assert_eq!(
|
|
render(&mut view),
|
|
"
|
|
[who_lives_in_a_pineapple_under_the_sea]
|
|
⏵ patrick_star
|
|
⏵ plankton
|
|
⏵ sandy_cheeks
|
|
⏵ (spongebob_squarepants)
|
|
"
|
|
.trim()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test_tree {
|
|
use helix_core::movement::Direction;
|
|
|
|
use super::Tree;
|
|
|
|
#[test]
|
|
fn test_get() {
|
|
let result = Tree::new(
|
|
"root",
|
|
vec![
|
|
Tree::new("foo", vec![Tree::new("bar", vec![])]),
|
|
Tree::new(
|
|
"spam",
|
|
vec![Tree::new("jar", vec![Tree::new("yo", vec![])])],
|
|
),
|
|
],
|
|
);
|
|
assert_eq!(result.get(0).unwrap().item, "root");
|
|
assert_eq!(result.get(1).unwrap().item, "foo");
|
|
assert_eq!(result.get(2).unwrap().item, "bar");
|
|
assert_eq!(result.get(3).unwrap().item, "spam");
|
|
assert_eq!(result.get(4).unwrap().item, "jar");
|
|
assert_eq!(result.get(5).unwrap().item, "yo");
|
|
}
|
|
|
|
#[test]
|
|
fn test_iter() {
|
|
let tree = Tree::new(
|
|
"spam",
|
|
vec![
|
|
Tree::new("jar", vec![Tree::new("yo", vec![])]),
|
|
Tree::new("foo", vec![Tree::new("bar", vec![])]),
|
|
],
|
|
);
|
|
|
|
let mut iter = tree.iter();
|
|
assert_eq!(iter.next().map(|tree| tree.item), Some("spam"));
|
|
assert_eq!(iter.next().map(|tree| tree.item), Some("jar"));
|
|
assert_eq!(iter.next().map(|tree| tree.item), Some("yo"));
|
|
assert_eq!(iter.next().map(|tree| tree.item), Some("foo"));
|
|
assert_eq!(iter.next().map(|tree| tree.item), Some("bar"));
|
|
|
|
assert_eq!(iter.next().map(|tree| tree.item), None)
|
|
}
|
|
|
|
#[test]
|
|
fn test_iter_double_ended() {
|
|
let tree = Tree::new(
|
|
"spam",
|
|
vec![
|
|
Tree::new("jar", vec![Tree::new("yo", vec![])]),
|
|
Tree::new("foo", vec![Tree::new("bar", vec![])]),
|
|
],
|
|
);
|
|
|
|
let mut iter = tree.iter();
|
|
assert_eq!(iter.next_back().map(|tree| tree.item), Some("bar"));
|
|
assert_eq!(iter.next_back().map(|tree| tree.item), Some("foo"));
|
|
assert_eq!(iter.next_back().map(|tree| tree.item), Some("yo"));
|
|
assert_eq!(iter.next_back().map(|tree| tree.item), Some("jar"));
|
|
assert_eq!(iter.next_back().map(|tree| tree.item), Some("spam"));
|
|
assert_eq!(iter.next_back().map(|tree| tree.item), None)
|
|
}
|
|
|
|
#[test]
|
|
fn test_len() {
|
|
let tree = Tree::new(
|
|
"spam",
|
|
vec![
|
|
Tree::new("jar", vec![Tree::new("yo", vec![])]),
|
|
Tree::new("foo", vec![Tree::new("bar", vec![])]),
|
|
],
|
|
);
|
|
|
|
assert_eq!(tree.len(), 5)
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_forward() {
|
|
let tree = Tree::new(
|
|
".cargo",
|
|
vec![
|
|
Tree::new("jar", vec![Tree::new("Cargo.toml", vec![])]),
|
|
Tree::new("Cargo.toml", vec![Tree::new("bar", vec![])]),
|
|
],
|
|
);
|
|
let result = tree.find(0, Direction::Forward, |tree| {
|
|
tree.item.to_lowercase().contains(&"cargo".to_lowercase())
|
|
});
|
|
|
|
assert_eq!(result, Some(0));
|
|
|
|
let result = tree.find(1, Direction::Forward, |tree| {
|
|
tree.item.to_lowercase().contains(&"cargo".to_lowercase())
|
|
});
|
|
|
|
assert_eq!(result, Some(2));
|
|
|
|
let result = tree.find(2, Direction::Forward, |tree| {
|
|
tree.item.to_lowercase().contains(&"cargo".to_lowercase())
|
|
});
|
|
|
|
assert_eq!(result, Some(2));
|
|
|
|
let result = tree.find(3, Direction::Forward, |tree| {
|
|
tree.item.to_lowercase().contains(&"cargo".to_lowercase())
|
|
});
|
|
|
|
assert_eq!(result, Some(3));
|
|
|
|
let result = tree.find(4, Direction::Forward, |tree| {
|
|
tree.item.to_lowercase().contains(&"cargo".to_lowercase())
|
|
});
|
|
|
|
assert_eq!(result, Some(0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_backward() {
|
|
let tree = Tree::new(
|
|
".cargo",
|
|
vec![
|
|
Tree::new("jar", vec![Tree::new("Cargo.toml", vec![])]),
|
|
Tree::new("Cargo.toml", vec![Tree::new("bar", vec![])]),
|
|
],
|
|
);
|
|
let result = tree.find(0, Direction::Backward, |tree| {
|
|
tree.item.to_lowercase().contains(&"cargo".to_lowercase())
|
|
});
|
|
|
|
assert_eq!(result, Some(3));
|
|
|
|
let result = tree.find(1, Direction::Backward, |tree| {
|
|
tree.item.to_lowercase().contains(&"cargo".to_lowercase())
|
|
});
|
|
|
|
assert_eq!(result, Some(0));
|
|
|
|
let result = tree.find(2, Direction::Backward, |tree| {
|
|
tree.item.to_lowercase().contains(&"cargo".to_lowercase())
|
|
});
|
|
|
|
assert_eq!(result, Some(0));
|
|
|
|
let result = tree.find(3, Direction::Backward, |tree| {
|
|
tree.item.to_lowercase().contains(&"cargo".to_lowercase())
|
|
});
|
|
|
|
assert_eq!(result, Some(2));
|
|
|
|
let result = tree.find(4, Direction::Backward, |tree| {
|
|
tree.item.to_lowercase().contains(&"cargo".to_lowercase())
|
|
});
|
|
|
|
assert_eq!(result, Some(3));
|
|
}
|
|
}
|