aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/ui/tree.rs
diff options
context:
space:
mode:
authorwongjiahau2024-05-01 20:42:31 +0000
committerJJ2024-05-01 23:06:08 +0000
commit3bff36ab90aba7de8bb5bff7dbb8230d81cdf582 (patch)
tree733bdb4ff30a35c2212910d48a635ddf4c23b2e3 /helix-term/src/ui/tree.rs
parent2cadec0b1182332338a5a1cc3062776f834d8835 (diff)
Add file explorer and tree helper
ref: https://github.com/helix-editor/helix/issues/200 ref: https://github.com/helix-editor/helix/pull/2377 ref: https://github.com/helix-editor/helix/pull/5566 ref: https://github.com/helix-editor/helix/pull/5768 Co-authored-by: cossonleo <cossonleo@foxmail.com> Co-authored-by: JJ <git@toki.la> Co-authored-by: Quan Tong <quantonganh@gmail.com>
Diffstat (limited to 'helix-term/src/ui/tree.rs')
-rw-r--r--helix-term/src/ui/tree.rs2681
1 files changed, 2681 insertions, 0 deletions
diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs
new file mode 100644
index 00000000..d0a9af5b
--- /dev/null
+++ b/helix-term/src/ui/tree.rs
@@ -0,0 +1,2681 @@
+use std::cmp::Ordering;
+
+use anyhow::Result;
+use helix_view::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,
+
+ #[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(),
+ })
+ }
+
+ 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
+ }
+
+ /// 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<()> {
+ // 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,
+}
+struct RenderTreeParams<'a, T> {
+ tree: &'a Tree<T>,
+ prefix: &'a String,
+ level: usize,
+ selected: usize,
+}
+
+fn render_tree<T: TreeViewItem>(
+ RenderTreeParams {
+ tree,
+ prefix,
+ level,
+ selected,
+ }: RenderTreeParams<T>,
+) -> Vec<RenderedLine> {
+ let indent = if level > 0 {
+ let indicator = if tree.item().is_parent() {
+ if tree.is_opened {
+ "⏷"
+ } else {
+ "⏵"
+ }
+ } else {
+ " "
+ };
+ format!("{}{} ", prefix, indicator)
+ } else {
+ "".to_string()
+ };
+ let name = tree.item.name();
+ let head = RenderedLine {
+ indent,
+ selected: selected == tree.index,
+ is_ancestor_of_current_item: selected != tree.index && tree.get(selected).is_some(),
+ content: name,
+ };
+ 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,
+ })
+ }))
+ .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 style = if line.selected {
+ style.add_modifier(Modifier::REVERSED)
+ } else {
+ style
+ };
+ let x = area.x.saturating_add(indent_len);
+ surface.set_stringn(
+ x,
+ area.y,
+ line.content.clone(),
+ area.width
+ .saturating_sub(indent_len)
+ .saturating_sub(1)
+ .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,
+ };
+
+ 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));
+ }
+}