- refactor(explore):Move filter to Tree
- feat(explore): Implement mkdir -p (but not tested yet)
- feat(ui/tree): Implement jump backward
- test(ui/tree): Refresh
pull/9/head
wongjiahau 1 year ago
parent 2e654a0775
commit 2e7709e505

@ -27,6 +27,9 @@ TODO
- [x] Remove comments
- [x] fix warnings
- [x] refactor, add tree.expand_children() method
- [] Change '[' to "go to previous root"
- [] Change 'b' to "go to parent"
- [] Use C-o for jumping to previous position
- [] add integration testing (test explorer rendering)
- [] search highlight matching word
- [] Error didn't clear
@ -37,4 +40,5 @@ TODO
- [] Fix panic bugs (see github comments)
- [] Sticky ancestors
- [] Ctrl-o should work for 'h', 'gg', 'ge', etc
- [] explorer(previow): overflow where bufferline is there
- [] explorer(preview): content not sorted

@ -3,7 +3,7 @@ use crate::{
compositor::{Component, Context, EventResult},
ctrl, key, shift, ui,
};
use anyhow::{bail, ensure, Result};
use anyhow::{ensure, Result};
use helix_core::Position;
use helix_view::{
editor::{Action, ExplorerPositionEmbed},
@ -122,21 +122,11 @@ impl TreeViewItem for FileInfo {
#[derive(Clone, Debug)]
enum PromptAction {
Search {
search_next: bool,
}, // search next/search pre
CreateFolder {
folder_path: PathBuf,
parent_index: usize,
},
CreateFile {
folder_path: PathBuf,
parent_index: usize,
},
CreateFolder { folder_path: PathBuf },
CreateFile { folder_path: PathBuf },
RemoveDir,
RemoveFile(Option<DocumentId>),
RenameFile(Option<DocumentId>),
Filter,
}
#[derive(Clone, Debug)]
@ -288,11 +278,9 @@ impl Explorer {
("A", "Add folder"),
("r", "Rename file/folder"),
("d", "Delete file"),
("f", "Filter"),
("[", "Change root to parent folder"),
("b", "Change root to parent folder"),
("]", "Change root to current folder"),
("C-o", "Go to previous root"),
("R", "Refresh"),
("[", "Go to previous root"),
("+", "Increase size"),
("-", "Decrease size"),
("q", "Close"),
@ -326,28 +314,10 @@ impl Explorer {
})
}
fn new_search_prompt(&mut self, search_next: bool) {
// self.tree.save_view();
self.prompt = Some((
PromptAction::Search { search_next },
Prompt::new(" Search: ".into(), None, ui::completers::none, |_, _, _| {}),
))
}
fn new_filter_prompt(&mut self, cx: &mut Context) {
// self.tree.save_view();
self.prompt = Some((
PromptAction::Filter,
Prompt::new(" Filter: ".into(), None, ui::completers::none, |_, _, _| {})
.with_line(self.state.filter.clone(), cx.editor),
))
}
fn new_create_folder_prompt(&mut self) -> Result<()> {
let (parent_index, folder_path) = self.nearest_folder()?;
let folder_path = self.nearest_folder()?;
self.prompt = Some((
PromptAction::CreateFolder {
parent_index,
folder_path: folder_path.clone(),
},
Prompt::new(
@ -361,10 +331,9 @@ impl Explorer {
}
fn new_create_file_prompt(&mut self) -> Result<()> {
let (parent_index, folder_path) = self.nearest_folder()?;
let folder_path = self.nearest_folder()?;
self.prompt = Some((
PromptAction::CreateFile {
parent_index,
folder_path: folder_path.clone(),
},
Prompt::new(
@ -377,24 +346,18 @@ impl Explorer {
Ok(())
}
fn nearest_folder(&self) -> Result<(usize, PathBuf)> {
fn nearest_folder(&self) -> Result<PathBuf> {
let current = self.tree.current();
if current.item().is_parent() {
Ok((current.index(), current.item().path.to_path_buf()))
Ok(current.item().path.to_path_buf())
} else {
let parent_index = current.parent_index().ok_or_else(|| {
anyhow::anyhow!(format!(
"Unable to get parent index of '{}'",
current.item().path.to_string_lossy()
))
})?;
let parent_path = current.item().path.parent().ok_or_else(|| {
anyhow::anyhow!(format!(
"Unable to get parent path of '{}'",
current.item().path.to_string_lossy()
))
})?;
Ok((parent_index, parent_path.to_path_buf()))
Ok(parent_path.to_path_buf())
}
}
@ -522,15 +485,8 @@ impl Explorer {
area.width.into(),
title_style,
);
surface.set_stringn(
area.x,
area.y.saturating_add(1),
format!("[FILTER]: {}", self.state.filter),
area.width.into(),
cx.editor.theme.get("ui.text"),
);
self.tree
.render(area.clip_top(2), surface, cx, &self.state.filter);
.render(area.clip_top(1), surface, cx, &self.state.filter);
}
pub fn render_embed(
@ -646,60 +602,7 @@ impl Explorer {
EventResult::Consumed(None)
}
fn handle_search_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult {
let (action, mut prompt) = self.prompt.take().unwrap();
let search_next = match action {
PromptAction::Search { search_next } => search_next,
_ => return EventResult::Ignored(None),
};
match event {
key!(Tab) | key!(Down) | ctrl!('j') => {
let filter = self.state.filter.clone();
return self
.tree
.handle_event(&Event::Key(*event), cx, &mut self.state, &filter);
}
key!(Enter) => {
let search_str = prompt.line().clone();
if !search_str.is_empty() {
self.repeat_motion = Some(Box::new(move |explorer, action, _| {
if let PromptAction::Search {
search_next: is_next,
} = action
{
// explorer.tree.save_view();
if is_next == search_next {
// explorer.tree.search_next(&search_str);
} else {
// explorer.tree.search_previous(&search_str);
}
}
}))
} else {
self.repeat_motion = None;
}
}
// key!(Esc) | ctrl!('c') => self.tree.restore_view(),
_ => {
if let EventResult::Consumed(_) = prompt.handle_event(&Event::Key(*event), cx) {
if search_next {
// self.tree.search_next(prompt.line());
} else {
// self.tree.search_previous(prompt.line());
}
}
self.prompt = Some((action, prompt));
}
};
EventResult::Consumed(None)
}
fn handle_prompt_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult {
match &self.prompt {
Some((PromptAction::Search { .. }, _)) => return self.handle_search_event(event, cx),
Some((PromptAction::Filter, _)) => return self.handle_filter_event(event, cx),
_ => {}
};
fn handle_prompt_event(
explorer: &mut Explorer,
event: &KeyEvent,
@ -711,20 +614,12 @@ impl Explorer {
};
let line = prompt.line();
match (&action, event) {
(
PromptAction::CreateFolder {
folder_path,
parent_index,
},
key!(Enter),
) => explorer.new_path(folder_path.clone(), line, true, *parent_index)?,
(
PromptAction::CreateFile {
folder_path,
parent_index,
},
key!(Enter),
) => explorer.new_path(folder_path.clone(), line, false, *parent_index)?,
(PromptAction::CreateFolder { folder_path }, key!(Enter)) => {
explorer.new_path(folder_path.clone(), line, true)?
}
(PromptAction::CreateFile { folder_path }, key!(Enter)) => {
explorer.new_path(folder_path.clone(), line, false)?
}
(PromptAction::RemoveDir, key!(Enter)) => {
if line == "y" {
let item = explorer.tree.current_item();
@ -768,30 +663,19 @@ impl Explorer {
}
}
fn new_path(
&mut self,
current_parent: PathBuf,
file_name: &str,
is_dir: bool,
parent_index: usize,
) -> Result<()> {
fn new_path(&mut self, current_parent: PathBuf, file_name: &str, is_dir: bool) -> Result<()> {
let path = helix_core::path::get_normalized_path(&current_parent.join(file_name));
match path.parent() {
Some(p) if p == current_parent => {}
_ => bail!("The file name is not illegal"),
};
let file = if is_dir {
std::fs::create_dir(&path)?;
FileInfo::new(path, FileType::Folder)
if is_dir {
std::fs::create_dir_all(&path)?;
} else {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut fd = std::fs::OpenOptions::new();
fd.create_new(true).write(true).open(&path)?;
FileInfo::new(path, FileType::File)
};
self.tree
.add_child(parent_index, file, &self.state.filter)?;
Ok(())
self.reveal_file(path)
}
fn toggle_help(&mut self) {
@ -851,7 +735,6 @@ impl Component for Explorer {
match key_event {
key!(Esc) => self.unfocus(),
key!('q') => self.close(),
key!('f') => self.new_filter_prompt(cx),
key!('?') => self.toggle_help(),
key!('a') => {
if let Err(error) = self.new_create_file_prompt() {
@ -863,21 +746,16 @@ impl Component for Explorer {
cx.editor.set_error(error.to_string())
}
}
key!('[') => {
key!('b') => {
if let Some(parent) = self.state.current_root.parent().clone() {
let path = parent.to_path_buf();
self.change_root(cx, path)
}
}
key!(']') => self.change_root(cx, self.tree.current_item().path.clone()),
ctrl!('o') => self.go_to_previous_root(),
key!('[') => self.go_to_previous_root(),
key!('d') => self.new_remove_prompt(cx),
key!('r') => self.new_rename_prompt(cx),
shift!('R') => {
if let Err(error) = self.tree.refresh(&self.state.filter) {
cx.editor.set_error(error.to_string())
}
}
key!('-') => self.decrease_size(),
key!('+') => self.increase_size(),
_ => {

@ -296,12 +296,6 @@ impl<T> Tree<T> {
}
}
#[derive(Clone, Debug)]
enum PromptAction {
Search { search_next: bool },
Filter,
}
#[derive(Clone, Debug)]
struct SavedView {
selected: usize,
@ -311,13 +305,19 @@ struct SavedView {
pub struct TreeView<T: TreeViewItem> {
tree: Tree<T>,
prompt: Option<(PromptAction, Prompt)>,
search_prompt: Option<(Direction, Prompt)>,
filter_prompt: Option<Prompt>,
search_str: String,
filter: String,
/// Selected item idex
selected: usize,
history: Vec<usize>,
saved_view: Option<SavedView>,
/// For implementing vertical scroll
@ -345,6 +345,7 @@ impl<T: TreeViewItem> TreeView<T> {
Self {
tree: Tree::new(root, items),
selected: 0,
history: vec![],
saved_view: None,
winline: 0,
column: 0,
@ -355,8 +356,10 @@ impl<T: TreeViewItem> TreeView<T> {
on_opened_fn: None,
on_folded_fn: None,
on_next_key: None,
prompt: None,
search_str: "".to_owned(),
search_prompt: None,
filter_prompt: None,
search_str: "".into(),
filter: "".into(),
}
}
@ -394,7 +397,7 @@ impl<T: TreeViewItem> TreeView<T> {
/// vec!["helix-term", "src", "ui", "tree.rs"]
/// ```
pub fn reveal_item(&mut self, segments: Vec<&str>, filter: &String) -> Result<()> {
self.tree.refresh(filter)?;
self.refresh(filter)?;
// Expand the tree
segments.iter().fold(
@ -480,7 +483,9 @@ impl<T: TreeViewItem> TreeView<T> {
}
pub fn refresh(&mut self, filter: &String) -> Result<()> {
self.tree.refresh(filter)
self.tree.refresh(filter)?;
self.set_selected(self.selected);
Ok(())
}
fn move_to_first(&mut self) {
@ -510,8 +515,20 @@ pub fn tree_view_help() -> Vec<(&'static str, &'static str)> {
("k, up", "Up"),
("h, left", "Go to parent"),
("l, right", "Expand"),
("L", "Scroll right"),
("f", "Filter"),
("/", "Search"),
("n", "Go to next search match"),
("N", "Go to previous search match"),
("R", "Refresh"),
("H", "Scroll left"),
("L", "Scroll right"),
("Home", "Scroll to the leftmost"),
("End", "Scroll to the rightmost"),
("C-o", "Jump backward"),
("C-d", "Half page down"),
("C-u", "Half page up"),
("PageUp", "Full page up"),
("PageDown", "Full page down"),
("zz", "Align view center"),
("zt", "Align view top"),
("zb", "Align view bottom"),
@ -519,11 +536,6 @@ pub fn tree_view_help() -> Vec<(&'static str, &'static str)> {
("ge", "Go to last line"),
("gh", "Go to line start"),
("gl", "Go to line end"),
("C-d", "Page down"),
("C-u", "Page up"),
("/", "Search"),
("n", "Go to next search match"),
("N", "Go to previous search match"),
]
}
@ -608,6 +620,15 @@ impl<T: TreeViewItem> TreeView<T> {
}
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.history.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(
@ -621,7 +642,13 @@ impl<T: TreeViewItem> TreeView<T> {
.saturating_sub(self.selected.saturating_sub(selected)),
);
}
self.selected = selected;
self.selected = selected
}
fn jump_backward(&mut self) {
if let Some(index) = self.history.pop() {
self.set_selected_without_history(index);
}
}
fn move_up(&mut self, rows: usize) {
@ -674,11 +701,15 @@ impl<T: TreeViewItem> TreeView<T> {
}
fn get(&self, index: usize) -> &Tree<T> {
self.tree.get(index).unwrap()
self.tree
.get(index)
.expect(format!("Tree: index {index} is out of bound").as_str())
}
fn get_mut(&mut self, index: usize) -> &mut Tree<T> {
self.tree.get_mut(index).unwrap()
self.tree
.get_mut(index)
.expect(format!("Tree: index {index} is out of bound").as_str())
}
pub fn current(&self) -> &Tree<T> {
@ -809,23 +840,38 @@ fn render_tree<T: TreeViewItem>(
impl<T: TreeViewItem + Clone> TreeView<T> {
pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context, filter: &String) {
let style = cx.editor.theme.get(&self.tree_symbol_style);
let prompt_area = area.with_height(1);
if let Some((_, prompt)) = self.prompt.as_mut() {
surface.set_style(prompt_area, style.add_modifier(Modifier::REVERSED));
prompt.render_prompt(prompt_area, surface, cx)
let filter_prompt_area = area.with_height(1);
if let Some(prompt) = self.filter_prompt.as_mut() {
surface.set_style(filter_prompt_area, style.add_modifier(Modifier::REVERSED));
prompt.render_prompt(filter_prompt_area, surface, cx)
} else {
surface.set_stringn(
filter_prompt_area.x,
filter_prompt_area.y,
format!("[FILTER]: {}", self.filter.clone()),
filter_prompt_area.width as usize,
style,
);
}
let search_prompt_area = area.clip_top(1).with_height(1);
if let Some((_, prompt)) = self.search_prompt.as_mut() {
surface.set_style(search_prompt_area, style.add_modifier(Modifier::REVERSED));
prompt.render_prompt(search_prompt_area, surface, cx)
} else {
surface.set_stringn(
prompt_area.x,
prompt_area.y,
search_prompt_area.x,
search_prompt_area.y,
format!("[SEARCH]: {}", self.search_str.clone()),
prompt_area.width as usize,
search_prompt_area.width as usize,
style,
);
}
let ancestor_style = cx.editor.theme.get("ui.text.focus");
let area = area.clip_top(1);
let area = area.clip_top(2);
let iter = self.render_lines(area, filter).into_iter().enumerate();
for (index, line) in iter {
@ -958,9 +1004,14 @@ impl<T: TreeViewItem + Clone> TreeView<T> {
return EventResult::Consumed(None);
}
if let EventResult::Consumed(c) = self.handle_prompt_event(key_event, cx) {
if let EventResult::Consumed(c) = self.handle_search_event(key_event, cx) {
return EventResult::Consumed(c);
}
if let EventResult::Consumed(c) = self.handle_filter_event(key_event, cx) {
return 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() as usize + count * 10,
@ -993,64 +1044,115 @@ impl<T: TreeViewItem + Clone> TreeView<T> {
_ => {}
}));
}
key!('/') => self.new_search_prompt(true),
key!('/') => self.new_search_prompt(Direction::Forward),
key!('n') => self.move_to_next_search_match(),
shift!('N') => self.move_to_previous_next_match(),
key!('f') => self.new_filter_prompt(cx),
key!(PageDown) => self.move_down_page(),
key!(PageUp) => self.move_up_page(),
shift!('R') => {
let filter = self.filter.clone();
if let Err(error) = self.refresh(&filter) {
cx.editor.set_error(error.to_string())
}
}
key!(Home) => self.move_leftmost(),
key!(End) => self.move_rightmost(),
ctrl!('o') => self.jump_backward(),
_ => return EventResult::Ignored(None),
}
EventResult::Consumed(None)
}
fn handle_prompt_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult {
match &self.prompt {
Some((PromptAction::Search { .. }, _)) => return self.handle_search_event(event, cx),
// Some((PromptAction::Filter, _)) => return self.handle_filter_event(event, cx),
_ => EventResult::Ignored(None),
fn handle_filter_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult {
if let Some(mut prompt) = self.filter_prompt.take() {
(|| -> Result<()> {
match event {
key!(Enter) => {
if let EventResult::Consumed(_) =
prompt.handle_event(&Event::Key(*event), cx)
{
self.refresh(prompt.line())?;
}
}
key!(Esc) | ctrl!('c') => {
self.filter.clear();
self.refresh(&"".to_string())?;
}
_ => {
if let EventResult::Consumed(_) =
prompt.handle_event(&Event::Key(*event), cx)
{
self.refresh(prompt.line())?;
}
self.filter = prompt.line().clone();
self.filter_prompt = Some(prompt);
}
};
Ok(())
})()
.unwrap_or_else(|err| cx.editor.set_error(format!("{err}")));
EventResult::Consumed(None)
} else {
EventResult::Ignored(None)
}
}
fn handle_search_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult {
let (action, mut prompt) = self.prompt.take().unwrap();
let search_next = match action {
PromptAction::Search { search_next } => search_next,
_ => return EventResult::Ignored(None),
};
match event {
key!(Enter) => {
self.set_search_str(prompt.line().clone());
EventResult::Consumed(None)
}
key!(Esc) => EventResult::Consumed(None),
_ => {
let event = prompt.handle_event(&Event::Key(*event), cx);
let line = prompt.line();
if search_next {
self.search_next(line)
} else {
self.search_previous(line)
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) => 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
}
self.prompt = Some((action, prompt));
event
}
} else {
EventResult::Ignored(None)
}
}
fn new_search_prompt(&mut self, search_next: bool) {
fn new_search_prompt(&mut self, direction: Direction) {
self.save_view();
self.prompt = Some((
PromptAction::Search { search_next },
self.search_prompt = Some((
direction,
Prompt::new(
"[SEARCH]: ".into(),
None,
ui::completers::theme,
ui::completers::none,
|_, _, _| {},
),
))
}
fn new_filter_prompt(&mut self, cx: &mut Context) {
self.save_view();
self.filter_prompt = Some(
Prompt::new(
"[FILTER]: ".into(),
None,
ui::completers::none,
|_, _, _| {},
)
.with_line(self.filter.clone(), cx.editor),
)
}
pub fn prompting(&self) -> bool {
self.prompt.is_some()
self.filter_prompt.is_some() || self.search_prompt.is_some()
}
}
@ -1830,6 +1932,86 @@ krabby_patty
.trim()
);
}
#[test]
fn test_refresh() {
let mut view = dummy_tree_view();
// 1. Move to the last child item on the tree
view.move_to_last();
view.move_to_children(&"".to_string()).unwrap();
view.move_to_last();
view.move_to_children(&"".to_string()).unwrap();
view.move_to_last();
view.move_to_children(&"".to_string()).unwrap();
view.move_to_last();
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
[squar]
sq
[uar]
(ar)"
.trim_start_matches(|c| c == '\n')
);
// 2. Refreshes the tree with a filter that will remove the last child
view.refresh(&"ar".to_string()).unwrap();
// 3. Get the current item
let item = view.current_item();
// 3a. Expects no failure
assert_eq!(item.name, "who_lives_in_a_pine")
}
#[test]
fn test_jump_backward() {
let mut view = dummy_tree_view();
view.move_down_half_page();
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()
);
}
}
#[cfg(test)]

Loading…
Cancel
Save