diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 9b447947..e52f8a7e 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1383,6 +1383,12 @@ impl Component for EditorView { if let Some(explorer) = self.explorer.as_mut() { if !explorer.content.is_focus() { if let Some(position) = config.explorer.is_embed() { + let area = if use_bufferline { + area.clip_top(1) + } + else { + area + }; explorer.content.render_embed(area, surface, cx, &position); } } @@ -1470,6 +1476,12 @@ impl Component for EditorView { if let Some(explore) = self.explorer.as_mut() { if explore.content.is_focus() { if let Some(position) = config.explorer.is_embed() { + let area = if use_bufferline { + area.clip_top(1) + } + else { + area + }; explore.content.render_embed(area, surface, cx, &position); } else { explore.render(area, surface, cx); diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index 9da83af1..62da7da7 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -9,6 +9,7 @@ use helix_view::{ editor::{Action, ExplorerPositionEmbed}, graphics::{CursorKind, Rect}, input::{Event, KeyEvent}, + theme::Modifier, DocumentId, Editor, }; use std::borrow::Cow; @@ -287,11 +288,10 @@ impl Explorer { ("A", "Add folder"), ("r", "Rename file/folder"), ("d", "Delete file"), - ("/", "Search"), ("f", "Filter"), ("[", "Change root to parent folder"), ("]", "Change root to current folder"), - ("^o", "Go to previous root"), + ("C-o", "Go to previous root"), ("R", "Refresh"), ("+", "Increase size"), ("-", "Decrease size"), @@ -327,7 +327,7 @@ impl Explorer { } fn new_search_prompt(&mut self, search_next: bool) { - self.tree.save_view(); + // self.tree.save_view(); self.prompt = Some(( PromptAction::Search { search_next }, Prompt::new(" Search: ".into(), None, ui::completers::none, |_, _, _| {}), @@ -335,7 +335,7 @@ impl Explorer { } fn new_filter_prompt(&mut self, cx: &mut Context) { - self.tree.save_view(); + // self.tree.save_view(); self.prompt = Some(( PromptAction::Filter, Prompt::new(" Filter: ".into(), None, ui::completers::none, |_, _, _| {}) @@ -603,7 +603,7 @@ impl Explorer { if preview_area.width < 30 || preview_area.height < 3 { return; } - let y = self.tree.row().saturating_sub(1) as u16; + let y = self.tree.winline().saturating_sub(1) as u16; let y = if (preview_area_height + y) > preview_area.height { preview_area.height - preview_area_height } else { @@ -667,11 +667,11 @@ impl Explorer { search_next: is_next, } = action { - explorer.tree.save_view(); + // explorer.tree.save_view(); if is_next == search_next { - explorer.tree.search_next(&search_str); + // explorer.tree.search_next(&search_str); } else { - explorer.tree.search_previous(&search_str); + // explorer.tree.search_previous(&search_str); } } })) @@ -679,13 +679,13 @@ impl Explorer { self.repeat_motion = None; } } - key!(Esc) | ctrl!('c') => self.tree.restore_view(), + // 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()); + // self.tree.search_next(prompt.line()); } else { - self.tree.search_previous(prompt.line()); + // self.tree.search_previous(prompt.line()); } } self.prompt = Some((action, prompt)); @@ -828,6 +828,10 @@ impl Explorer { impl Component for Explorer { /// Process input events, return true if handled. fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { + let filter = self.state.filter.clone(); + if self.tree.prompting() { + return self.tree.handle_event(event, cx, &mut self.state, &filter); + } let key_event = match event { Event::Key(event) => event, Event::Resize(..) => return EventResult::Consumed(None), @@ -847,20 +851,7 @@ impl Component for Explorer { match key_event { key!(Esc) => self.unfocus(), key!('q') => self.close(), - key!('n') => { - if let Some(mut repeat_motion) = self.repeat_motion.take() { - repeat_motion(self, PromptAction::Search { search_next: true }, cx); - self.repeat_motion = Some(repeat_motion); - } - } - shift!('N') => { - if let Some(mut repeat_motion) = self.repeat_motion.take() { - repeat_motion(self, PromptAction::Search { search_next: false }, cx); - self.repeat_motion = Some(repeat_motion); - } - } key!('f') => self.new_filter_prompt(cx), - key!('/') => self.new_search_prompt(true), key!('?') => self.toggle_help(), key!('a') => { if let Err(error) = self.new_create_file_prompt() { @@ -890,7 +881,6 @@ impl Component for Explorer { key!('-') => self.decrease_size(), key!('+') => self.increase_size(), _ => { - let filter = self.state.filter.clone(); self.tree .handle_event(&Event::Key(*key_event), cx, &mut self.state, &filter); } diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 0aaabf0b..2d1917b2 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -4,8 +4,8 @@ use anyhow::Result; use helix_view::theme::Modifier; use crate::{ - compositor::{Context, EventResult}, - ctrl, key, shift, + compositor::{Component, Context, EventResult}, + ctrl, key, shift, ui, }; use helix_core::movement::Direction; use helix_view::{ @@ -14,6 +14,8 @@ use helix_view::{ }; use tui::buffer::Buffer as Surface; +use super::Prompt; + pub trait TreeViewItem: Sized { type Params; @@ -33,7 +35,7 @@ fn tree_item_cmp(item1: &T, item2: &T) -> Ordering { T::cmp(item1, item2) } -pub fn vec_to_tree(mut items: Vec) -> Vec> { +fn vec_to_tree(mut items: Vec) -> Vec> { items.sort_by(tree_item_cmp); index_elems( 0, @@ -212,17 +214,26 @@ impl Tree { /// 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`. - pub fn find(&self, start_index: usize, direction: Direction, predicate: F) -> Option + fn find(&self, start_index: usize, direction: Direction, predicate: F) -> Option where - F: FnMut(&Tree) -> bool, + F: Clone + FnMut(&Tree) -> bool, { - let iter = self.iter(); match direction { - Direction::Forward => iter + Direction::Forward => match self + .iter() .skip(start_index) - .position(predicate) - .map(|index| index + start_index), - Direction::Backward => iter.take(start_index).rposition(predicate), + .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), + }, } } @@ -285,14 +296,29 @@ impl Tree { } } +#[derive(Clone, Debug)] +enum PromptAction { + Search { search_next: bool }, + Filter, +} + +#[derive(Clone, Debug)] +struct SavedView { + selected: usize, + winline: usize, +} + pub struct TreeView { tree: Tree, + prompt: Option<(PromptAction, Prompt)>, + + search_str: String, + /// Selected item idex selected: usize, - /// (selected, row) - save_view: (usize, usize), + saved_view: Option, /// For implementing vertical scroll winline: usize, @@ -319,7 +345,7 @@ impl TreeView { Self { tree: Tree::new(root, items), selected: 0, - save_view: (0, 0), + saved_view: None, winline: 0, column: 0, max_len: 0, @@ -329,6 +355,8 @@ impl TreeView { on_opened_fn: None, on_folded_fn: None, on_next_key: None, + prompt: None, + search_str: "".to_owned(), } } @@ -493,6 +521,9 @@ pub fn tree_view_help() -> Vec<(&'static str, &'static str)> { ("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"), ] } @@ -529,29 +560,47 @@ impl TreeView { } } - pub fn search_next(&mut self, s: &str) { - let skip = std::cmp::max(2, self.save_view.0 + 1); + 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_else(|| 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(self.save_view.0), + .unwrap_or(saved_view.selected), ); - - self.winline = (self.save_view.1 + self.selected).saturating_sub(self.save_view.0); } - pub fn search_previous(&mut self, s: &str) { - let take = self.save_view.0; + 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(self.save_view.0), + .unwrap_or(saved_view.selected), ); + } + + fn move_to_next_search_match(&mut self) { + self.search_next(&self.search_str.clone()) + } - self.winline = (self.save_view.1 + self.selected).saturating_sub(self.save_view.0); + fn move_to_previous_next_match(&mut self) { + self.search_previous(&self.search_str.clone()) } - pub fn move_down(&mut self, rows: usize) { + fn move_down(&mut self, rows: usize) { let len = self.tree.len(); if len > 0 { self.set_selected(std::cmp::min(self.selected + rows, len.saturating_sub(1))) @@ -575,18 +624,18 @@ impl TreeView { self.selected = selected; } - pub fn move_up(&mut self, rows: usize) { + fn move_up(&mut self, rows: usize) { let len = self.tree.len(); if len > 0 { self.set_selected(self.selected.saturating_sub(rows).max(0)) } } - pub fn move_left(&mut self, cols: usize) { + fn move_left(&mut self, cols: usize) { self.column = self.column.saturating_sub(cols); } - pub fn move_right(&mut self, cols: usize) { + fn move_right(&mut self, cols: usize) { let max_scroll = self .max_len .saturating_sub(self.previous_area.width as usize) @@ -594,28 +643,34 @@ impl TreeView { self.column = max_scroll.min(self.column + cols); } - pub fn move_down_half_page(&mut self) { + fn move_down_half_page(&mut self) { self.move_down(self.previous_area.height as usize / 2) } - pub fn move_up_half_page(&mut self) { + fn move_up_half_page(&mut self) { self.move_up(self.previous_area.height as usize / 2); } - pub fn move_down_page(&mut self) { + fn move_down_page(&mut self) { self.move_down(self.previous_area.height as usize); } - pub fn move_up_page(&mut self) { + fn move_up_page(&mut self) { self.move_up(self.previous_area.height as usize); } - pub fn save_view(&mut self) { - self.save_view = (self.selected, self.winline); + fn save_view(&mut self) { + self.saved_view = Some(SavedView { + selected: self.selected, + winline: self.winline, + }) } - pub fn restore_view(&mut self) { - (self.selected, self.winline) = self.save_view; + fn restore_view(&mut self) { + SavedView { + selected: self.selected, + winline: self.winline, + } = self.saved_view(); } fn get(&self, index: usize) -> &Tree { @@ -646,7 +701,7 @@ impl TreeView { &self.current().item } - pub fn row(&self) -> usize { + pub fn winline(&self) -> usize { self.winline } @@ -754,12 +809,27 @@ 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) + } else { + surface.set_stringn( + prompt_area.x, + prompt_area.y, + format!("[SEARCH]: {}", self.search_str.clone()), + prompt_area.width as usize, + style, + ); + } + let ancestor_style = cx.editor.theme.get("ui.text.focus"); + let area = area.clip_top(1); let iter = self.render_lines(area, filter).into_iter().enumerate(); for (index, line) in iter { - let area = Rect::new(area.x, area.y + index as u16, area.width, 1); + 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, @@ -887,6 +957,10 @@ impl TreeView { on_next_key(cx, self, key_event); return EventResult::Consumed(None); } + + if let EventResult::Consumed(c) = self.handle_prompt_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, @@ -919,11 +993,65 @@ impl TreeView { _ => {} })); } + key!('/') => self.new_search_prompt(true), + key!('n') => self.move_to_next_search_match(), + shift!('N') => self.move_to_previous_next_match(), _ => 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_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) + } + self.prompt = Some((action, prompt)); + event + } + } + } + + fn new_search_prompt(&mut self, search_next: bool) { + self.save_view(); + self.prompt = Some(( + PromptAction::Search { search_next }, + Prompt::new( + "[SEARCH]: ".into(), + None, + ui::completers::theme, + |_, _, _| {}, + ), + )) + } + + pub fn prompting(&self) -> bool { + self.prompt.is_some() + } } /// Recalculate the index of each item of a tree. @@ -1535,6 +1663,173 @@ krabby_patty .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), + " + gary_the_snail + karen + king_neptune + krabby_patty + (larry_the_lobster) +" + .trim() + ); + + view.move_to_last(); + 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), + " + gary_the_snail + karen + king_neptune + krabby_patty + (larry_the_lobster) +" + .trim() + ); + + view.move_to_last(); + view.search_previous("krab"); + assert_eq!( + render(&mut view), + " + gary_the_snail + 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), + " + king_neptune + krabby_patty + larry_the_lobster + mrs_puff + (patrick_star) + " + .trim() + ); + + view.move_to_next_search_match(); + assert_eq!( + render(&mut view), + " + king_neptune + (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), + " + king_neptune + krabby_patty + larry_the_lobster + mrs_puff + (patrick_star) + " + .trim() + ); + + view.move_to_previous_next_match(); + assert_eq!( + render(&mut view), + " + king_neptune + (krabby_patty) + larry_the_lobster + mrs_puff + patrick_star + " + .trim() + ); + + view.move_to_previous_next_match(); + assert_eq!( + render(&mut view), + " + king_neptune + krabby_patty + larry_the_lobster + mrs_puff + (patrick_star) + " + .trim() + ); + } } #[cfg(test)] @@ -1579,6 +1874,9 @@ mod test_tree { 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")); + + // Expect the iterator to be cyclic, so next() should jump to first item + assert_eq!(iter.next().map(|tree| tree.item), Some("spam")) } #[test]