From cf9b60a3d1da94889aa9e9b50e4cd4b2f0a324fa Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sat, 25 Feb 2023 12:14:48 +0800 Subject: [PATCH] feat(tree): sticky ancestors --- changes | 5 +- helix-term/src/ui/explorer.rs | 8 +- helix-term/src/ui/tree.rs | 424 +++++++++++++++++++++++++++++----- 3 files changed, 377 insertions(+), 60 deletions(-) diff --git a/changes b/changes index 6429aa7f..ff97ff45 100644 --- a/changes +++ b/changes @@ -46,12 +46,15 @@ New: - [x] bind "o" to open/close file/folder - [x] bind "C-n/C-p" to up/down - [x] bind "="/"_" to zoom-in/zoom-out +- [x] Sticky ancestors +- [] Toggle preview - [] search highlight matching word - [] Error didn't clear - [] should preview be there by default? - [] Fix panic bugs (see github comments) -- [] Sticky ancestors +- [] explorer(preview): use popup instead of custom components - [] explorer(preview): overflow where bufferline is there - [] explorer(preview): implement scrolling C-j/C-k - [] symlink not showing - [] remove unwrap and expect +- [] bug(tree): zb does not work, because clash with explorer 'b' diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index e766eee6..d0d9aac1 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -593,8 +593,8 @@ impl Explorer { ("b", "Change root to parent folder"), ("]", "Change root to current folder"), ("[", "Go to previous root"), - ("+", "Increase size"), - ("-", "Decrease size"), + ("+, =", "Increase size"), + ("-, _", "Decrease size"), ("q", "Close"), ] .into_iter() @@ -1169,8 +1169,8 @@ mod test_explorer { assert_eq!( render(&mut explorer), " +[test-explorer/new_folder]  [styles] -  public  [sus.sass]  [a]  [b] @@ -1296,8 +1296,8 @@ mod test_explorer { assert_eq!( render(&mut explorer), " +[test-explorer/new_file]  [styles] -  a  b c  public diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index b68460e2..656efa33 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -471,11 +471,11 @@ impl TreeView { Ok(()) } - fn move_to_first(&mut self) { + fn move_to_first_line(&mut self) { self.move_up(usize::MAX / 2) } - fn move_to_last(&mut self) { + fn move_to_last_line(&mut self) { self.move_down(usize::MAX / 2) } @@ -712,11 +712,12 @@ impl TreeView { } } +#[derive(Clone)] struct RenderedLine { indent: String, - name: String, + content: String, selected: bool, - descendant_selected: bool, + is_ancestor_of_current_item: bool, } struct RenderTreeParams<'a, T> { tree: &'a Tree, @@ -753,8 +754,8 @@ fn render_tree( let head = RenderedLine { indent, selected: selected == tree.index, - descendant_selected: selected != tree.index && tree.get(selected).is_some(), - name, + 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] @@ -828,12 +829,12 @@ impl TreeView { surface.set_stringn( x, area.y, - line.name.clone(), + line.content.clone(), area.width .saturating_sub(indent_len) .saturating_sub(1) .into(), - if line.descendant_selected { + if line.is_ancestor_of_current_item { ancestor_style } else { style @@ -849,11 +850,11 @@ impl TreeView { .into_iter() .map(|line| { let name = if line.selected { - format!("({})", line.name) - } else if line.descendant_selected { - format!("[{}]", line.name) + format!("({})", line.content) + } else if line.is_ancestor_of_current_item { + format!("[{}]", line.content) } else { - line.name + line.content }; format!("{}{}", line.indent, name) }) @@ -884,15 +885,82 @@ impl TreeView { line.indent .chars() .count() - .saturating_add(line.name.chars().count()) + .saturating_add(line.content.chars().count()) }) .max() .unwrap_or(0); let max_width = area.width as usize; - lines + let take = area.height as usize; + + struct RetainAncestorResult { + skipped_ancestors: Vec, + remaining_lines: Vec, + } + fn retain_ancestors(lines: Vec, 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::>(); + + 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::>(); + + 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; @@ -907,18 +975,15 @@ impl TreeView { .take(max_width) .collect::() }, - name: line - .name + content: line + .content .chars() .skip(skip.saturating_sub(indent_len)) - .take((max_width.saturating_sub(indent_len)).clamp(0, line.name.len())) + .take((max_width.saturating_sub(indent_len)).clamp(0, line.content.len())) .collect::(), ..line } }) - // Vertical scroll - .skip(skip) - .take(area.height as usize) .collect() } @@ -972,8 +1037,8 @@ impl TreeView { ctrl!('u') => self.move_up_half_page(), key!('g') => { self.on_next_key = Some(Box::new(|_, tree, event| match event { - key!('g') => tree.move_to_first(), - key!('e') => tree.move_to_last(), + key!('g') => tree.move_to_first_line(), + key!('e') => tree.move_to_last_line(), key!('h') => tree.move_leftmost(), key!('l') => tree.move_rightmost(), _ => {} @@ -1103,7 +1168,7 @@ impl TreeView { /// yo (4) /// ``` fn index_elems(parent_index: usize, elems: Vec>) -> Vec> { - fn index_elems<'a, T>( + fn index_elems( current_index: usize, elems: Vec>, parent_index: usize, @@ -1248,7 +1313,7 @@ mod test_tree_view { assert_eq!( render(&mut view), " - gary_the_snail +[who_lives_in_a_pineapple_under_the_sea]  karen  king_neptune  krabby_patty @@ -1261,7 +1326,7 @@ mod test_tree_view { assert_eq!( render(&mut view), " - gary_the_snail +[who_lives_in_a_pineapple_under_the_sea]  karen  king_neptune  (krabby_patty) @@ -1274,11 +1339,11 @@ mod test_tree_view { assert_eq!( render(&mut view), " +[who_lives_in_a_pineapple_under_the_sea]  (gary_the_snail)  karen  king_neptune  krabby_patty - larry_the_lobster " .trim() ); @@ -1296,7 +1361,7 @@ mod test_tree_view { .trim() ); - view.move_to_first(); + view.move_to_first_line(); view.move_up(1); assert_eq!( render(&mut view), @@ -1310,12 +1375,12 @@ mod test_tree_view { .trim() ); - view.move_to_last(); + view.move_to_last_line(); view.move_down(1); assert_eq!( render(&mut view), " - mrs_puff +[who_lives_in_a_pineapple_under_the_sea]  patrick_star  plankton  sandy_cheeks @@ -1332,7 +1397,7 @@ mod test_tree_view { assert_eq!( render(&mut view), " - gary_the_snail +[who_lives_in_a_pineapple_under_the_sea]  karen  king_neptune  krabby_patty @@ -1345,7 +1410,7 @@ mod test_tree_view { assert_eq!( render(&mut view), " - king_neptune +[who_lives_in_a_pineapple_under_the_sea]  krabby_patty  (larry_the_lobster)  mrs_puff @@ -1358,7 +1423,7 @@ mod test_tree_view { assert_eq!( render(&mut view), " - gary_the_snail +[who_lives_in_a_pineapple_under_the_sea]  karen  king_neptune  krabby_patty @@ -1372,11 +1437,11 @@ mod test_tree_view { fn test_move_to_first_last() { let mut view = dummy_tree_view(); - view.move_to_last(); + view.move_to_last_line(); assert_eq!( render(&mut view), " - mrs_puff +[who_lives_in_a_pineapple_under_the_sea]  patrick_star  plankton  sandy_cheeks @@ -1385,7 +1450,7 @@ mod test_tree_view { .trim() ); - view.move_to_first(); + view.move_to_first_line(); assert_eq!( render(&mut view), " @@ -1432,7 +1497,7 @@ mod test_tree_view { assert_eq!( render(&mut view), " - karen +[who_lives_in_a_pineapple_under_the_sea]  king_neptune  krabby_patty  larry_the_lobster @@ -1445,7 +1510,7 @@ mod test_tree_view { assert_eq!( render(&mut view), " - karen +[who_lives_in_a_pineapple_under_the_sea]  king_neptune  (krabby_patty)  larry_the_lobster @@ -1458,11 +1523,11 @@ mod test_tree_view { assert_eq!( render(&mut view), " +[who_lives_in_a_pineapple_under_the_sea]  (karen)  king_neptune  krabby_patty  larry_the_lobster - mrs_puff " .trim() ); @@ -1525,7 +1590,7 @@ mod test_tree_view { .trim() ); - view.move_to_last(); + view.move_to_last_line(); view.move_to_parent(); assert_eq!( render(&mut view), @@ -1747,7 +1812,7 @@ krabby_patty assert_eq!( render(&mut view), " - gary_the_snail +[who_lives_in_a_pineapple_under_the_sea]  karen  king_neptune  krabby_patty @@ -1756,7 +1821,7 @@ krabby_patty .trim() ); - view.move_to_last(); + view.move_to_last_line(); view.search_next("who_lives"); assert_eq!( render(&mut view), @@ -1779,7 +1844,7 @@ krabby_patty assert_eq!( render(&mut view), " - gary_the_snail +[who_lives_in_a_pineapple_under_the_sea]  karen  king_neptune  krabby_patty @@ -1788,12 +1853,12 @@ krabby_patty .trim() ); - view.move_to_last(); + view.move_to_last_line(); view.search_previous("krab"); assert_eq!( render(&mut view), " - gary_the_snail +[who_lives_in_a_pineapple_under_the_sea]  karen  king_neptune  (krabby_patty) @@ -1825,7 +1890,7 @@ krabby_patty assert_eq!( render(&mut view), " - king_neptune +[who_lives_in_a_pineapple_under_the_sea]  krabby_patty  larry_the_lobster  mrs_puff @@ -1838,7 +1903,7 @@ krabby_patty assert_eq!( render(&mut view), " - king_neptune +[who_lives_in_a_pineapple_under_the_sea]  (krabby_patty)  larry_the_lobster  mrs_puff @@ -1857,7 +1922,7 @@ krabby_patty assert_eq!( render(&mut view), " - king_neptune +[who_lives_in_a_pineapple_under_the_sea]  krabby_patty  larry_the_lobster  mrs_puff @@ -1870,7 +1935,7 @@ krabby_patty assert_eq!( render(&mut view), " - king_neptune +[who_lives_in_a_pineapple_under_the_sea]  (krabby_patty)  larry_the_lobster  mrs_puff @@ -1883,7 +1948,7 @@ krabby_patty assert_eq!( render(&mut view), " - king_neptune +[who_lives_in_a_pineapple_under_the_sea]  krabby_patty  larry_the_lobster  mrs_puff @@ -1898,22 +1963,22 @@ krabby_patty let mut view = dummy_tree_view(); // 1. Move to the last child item on the tree - view.move_to_last(); + view.move_to_last_line(); view.move_to_children(&"".to_string()).unwrap(); - view.move_to_last(); + view.move_to_last_line(); view.move_to_children(&"".to_string()).unwrap(); - view.move_to_last(); + view.move_to_last_line(); view.move_to_children(&"".to_string()).unwrap(); - view.move_to_last(); + view.move_to_last_line(); view.move_to_children(&"".to_string()).unwrap(); // 1a. Expect the current selected item is the last child on the tree assert_eq!( render(&mut view), " -  epants + [spongebob_squarepants] +  [squarepants]  [squar] - sq  [uar] (ar)" .trim_start_matches(|c| c == '\n') @@ -1974,6 +2039,255 @@ krabby_patty .trim() ); } + + #[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 + + #[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] + struct Item<'a> { + name: &'a str, + children: Option>>, + } + + fn parent<'a>(name: &'a str, children: Vec>) -> Item<'a> { + Item { + name, + children: Some(children), + } + } + + fn child<'a>(name: &'a str) -> Item<'a> { + Item { + name, + children: None, + } + } + + impl<'a> TreeViewItem for Item<'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> { + match &self.children { + Some(children) => Ok(children.clone()), + None => Ok(vec![]), + } + } + + fn filter(&self, s: &str) -> bool { + self.name().to_lowercase().contains(&s.to_lowercase()) + } + } + + fn render<'a>(view: &mut TreeView>) -> String { + view.render_to_string(dummy_area().with_height(3), &"".to_string()) + } + + let mut view = TreeView::new( + parent("root", vec![]), + vec_to_tree(vec![ + parent("a", vec![child("aa"), child("ab")]), + parent( + "b", + vec![parent( + "ba", + vec![parent("baa", vec![child("baaa"), child("baab")])], + )], + ), + ]), + ); + + assert_eq!( + render(&mut view), + " +(root) + a + b + " + .trim() + ); + + // 1. Move down to "a", and expand it + let filter = "".to_string(); + view.move_down(1); + view.move_to_children(&filter).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(&filter).unwrap(); + assert_eq!( + render(&mut view), + " +[root] + [b] +  (ba) + " + .trim() + ); + + // 5. Move to the children of "ba", which is "baa" + view.move_to_children(&filter).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(&filter).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().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() + ); + } } #[cfg(test)]