diff --git a/changes b/changes index a436eafd..c3e5b379 100644 --- a/changes +++ b/changes @@ -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 diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index 62da7da7..2c030512 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -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), RenameFile(Option), - 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 { 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(¤t_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(), _ => { diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 2d1917b2..8405af7c 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -296,12 +296,6 @@ impl Tree { } } -#[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 { tree: Tree, - prompt: Option<(PromptAction, Prompt)>, + search_prompt: Option<(Direction, Prompt)>, + + filter_prompt: Option, search_str: String, + filter: String, + /// Selected item idex selected: usize, + history: Vec, + saved_view: Option, /// For implementing vertical scroll @@ -345,6 +345,7 @@ impl TreeView { Self { tree: Tree::new(root, items), selected: 0, + history: vec![], saved_view: None, winline: 0, column: 0, @@ -355,8 +356,10 @@ impl TreeView { 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 TreeView { /// 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 TreeView { } 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 TreeView { } 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 TreeView { .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 TreeView { } fn get(&self, index: usize) -> &Tree { - 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 { - 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 { @@ -809,23 +840,38 @@ fn render_tree( impl TreeView { 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 TreeView { 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 TreeView { _ => {} })); } - 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)]