diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eb759bc..539147b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +**fuzzy find files** + +![fuzzy-find](assets/fuzzy-find.gif) + **emojified commit message** ![emojified-commit-message](assets/emojified-commit-message.png) ## Added +- fuzzy find files ([#891](https://github.com/extrawurst/gitui/issues/891)) - visualize progress during async syntax highlighting ([#889](https://github.com/extrawurst/gitui/issues/889)) - added support for markdown emoji's in commits [[@andrewpollack](https://github.com/andrewpollack)] ([#768](https://github.com/extrawurst/gitui/issues/768)) - added scrollbar to revlog [[@ashvin021](https://github.com/ashvin021)] ([#868](https://github.com/extrawurst/gitui/issues/868)) diff --git a/assets/fuzzy-find.gif b/assets/fuzzy-find.gif new file mode 100644 index 00000000..04960f09 Binary files /dev/null and b/assets/fuzzy-find.gif differ diff --git a/src/app.rs b/src/app.rs index 4de96aa5..e2d4e9a0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -438,6 +438,7 @@ impl App { accessors!( self, [ + find_file_popup, msg, reset, commit, @@ -454,7 +455,6 @@ impl App { rename_branch_popup, select_branch_popup, revision_files_popup, - find_file_popup, tags_popup, options_popup, help, @@ -726,7 +726,8 @@ impl App { .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); } InternalEvent::FileFinderChanged(file) => { - self.files_tab.file_finder_update(file); + self.files_tab.file_finder_update(&file); + self.revision_files_popup.file_finder_update(&file); flags .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); } diff --git a/src/components/file_find_popup.rs b/src/components/file_find_popup.rs index 36079aa9..7d2e0eda 100644 --- a/src/components/file_find_popup.rs +++ b/src/components/file_find_popup.rs @@ -1,6 +1,6 @@ use super::{ visibility_blocking, CommandBlocking, CommandInfo, Component, - DrawableComponent, EventState, TextInputComponent, + DrawableComponent, EventState, ScrollType, TextInputComponent, }; use crate::{ keys::SharedKeyConfig, @@ -29,7 +29,8 @@ pub struct FileFindPopup { query: Option, theme: SharedTheme, files: Vec, - selection: Option, + selection: usize, + selected_index: Option, files_filtered: Vec, key_config: SharedKeyConfig, } @@ -58,8 +59,9 @@ impl FileFindPopup { theme, files: Vec::new(), files_filtered: Vec::new(), + selected_index: None, key_config, - selection: None, + selection: 0, } } @@ -94,22 +96,21 @@ impl FileFindPopup { }) }), ); - - self.refresh_selection(); - } else { - self.files_filtered - .extend(self.files.iter().enumerate().map(|a| a.0)); } + + self.selection = 0; + self.refresh_selection(); } fn refresh_selection(&mut self) { - let selection = self.files_filtered.first().copied(); + let selection = + self.files_filtered.get(self.selection).copied(); - if self.selection != selection { - self.selection = selection; + if self.selected_index != selection { + self.selected_index = selection; let file = self - .selection + .selected_index .and_then(|index| self.files.get(index)) .map(|f| f.path.clone()); @@ -129,6 +130,25 @@ impl FileFindPopup { Ok(()) } + + fn move_selection(&mut self, move_type: ScrollType) -> bool { + let new_selection = match move_type { + ScrollType::Up => self.selection.saturating_sub(1), + ScrollType::Down => self.selection.saturating_add(1), + _ => self.selection, + }; + + let new_selection = new_selection + .clamp(0, self.files_filtered.len().saturating_sub(1)); + + if new_selection != self.selection { + self.selection = new_selection; + self.refresh_selection(); + return true; + } + + false + } } impl DrawableComponent for FileFindPopup { @@ -138,9 +158,28 @@ impl DrawableComponent for FileFindPopup { area: Rect, ) -> Result<()> { if self.is_visible() { - const SIZE: (u16, u16) = (50, 25); - let area = - ui::centered_rect_absolute(SIZE.0, SIZE.1, area); + const MAX_SIZE: (u16, u16) = (50, 20); + + let any_hits = !self.files_filtered.is_empty(); + + let area = ui::centered_rect_absolute( + MAX_SIZE.0, MAX_SIZE.1, area, + ); + + let area = if any_hits { + area + } else { + Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(3), + Constraint::Percentage(100), + ] + .as_ref(), + ) + .split(area)[0] + }; f.render_widget(Clear, area); f.render_widget( @@ -155,7 +194,7 @@ impl DrawableComponent for FileFindPopup { area, ); - let area = Layout::default() + let chunks = Layout::default() .direction(Direction::Vertical) .constraints( [ @@ -169,45 +208,46 @@ impl DrawableComponent for FileFindPopup { vertical: 1, })); - self.find_text.draw(f, area[0])?; + self.find_text.draw(f, chunks[0])?; - let height = usize::from(area[1].height); - let width = usize::from(area[1].width); + if any_hits { + let title = + format!("Hits: {}", self.files_filtered.len()); - let items = - self.files_filtered.iter().take(height).map(|idx| { - let selected = self - .selection - .map_or(false, |selection| selection == *idx); - Span::styled( - Cow::from(trim_length_left( - self.files[*idx] - .path - .to_str() - .unwrap_or_default(), - width, - )), - self.theme.text(selected, false), - ) - }); + let height = usize::from(chunks[1].height); + let width = usize::from(chunks[1].width); - let title = format!( - "Hits: {}/{}", - height.min(self.files_filtered.len()), - self.files_filtered.len() - ); + let items = + self.files_filtered.iter().take(height).map( + |idx| { + let selected = self + .selected_index + .map_or(false, |index| index == *idx); + Span::styled( + Cow::from(trim_length_left( + self.files[*idx] + .path + .to_str() + .unwrap_or_default(), + width, + )), + self.theme.text(selected, false), + ) + }, + ); - ui::draw_list_block( - f, - area[1], - Block::default() - .title(Span::styled( - title, - self.theme.title(true), - )) - .borders(Borders::TOP), - items, - ); + ui::draw_list_block( + f, + chunks[1], + Block::default() + .title(Span::styled( + title, + self.theme.title(true), + )) + .borders(Borders::TOP), + items, + ); + } } Ok(()) } @@ -228,6 +268,12 @@ impl Component for FileFindPopup { ) .order(1), ); + + out.push(CommandInfo::new( + strings::commands::scroll(&self.key_config), + true, + true, + )); } visibility_blocking(self) @@ -243,6 +289,10 @@ impl Component for FileFindPopup { || *key == self.key_config.enter { self.hide(); + } else if *key == self.key_config.move_down { + self.move_selection(ScrollType::Down); + } else if *key == self.key_config.move_up { + self.move_selection(ScrollType::Up); } } diff --git a/src/components/revision_files.rs b/src/components/revision_files.rs index 0abcf457..c452b4bf 100644 --- a/src/components/revision_files.rs +++ b/src/components/revision_files.rs @@ -146,10 +146,10 @@ impl RevisionFilesComponent { .push(InternalEvent::OpenFileFinder(self.files.clone())); } - pub fn find_file(&mut self, file: Option) { + pub fn find_file(&mut self, file: &Option) { if let Some(file) = file { self.tree.collapse_but_root(); - if self.tree.select_file(&file) { + if self.tree.select_file(file) { self.selection_changed(); } } diff --git a/src/components/revision_files_popup.rs b/src/components/revision_files_popup.rs index 210262ea..054ef852 100644 --- a/src/components/revision_files_popup.rs +++ b/src/components/revision_files_popup.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use super::{ revision_files::RevisionFilesComponent, visibility_blocking, CommandBlocking, CommandInfo, Component, DrawableComponent, @@ -59,6 +61,10 @@ impl RevisionFilesPopup { pub fn any_work_pending(&self) -> bool { self.files.any_work_pending() } + + pub fn file_finder_update(&mut self, file: &Option) { + self.files.find_file(file); + } } impl DrawableComponent for RevisionFilesPopup { diff --git a/src/tabs/files.rs b/src/tabs/files.rs index eb405eab..4838c767 100644 --- a/src/tabs/files.rs +++ b/src/tabs/files.rs @@ -71,7 +71,7 @@ impl FilesTab { } } - pub fn file_finder_update(&mut self, file: Option) { + pub fn file_finder_update(&mut self, file: &Option) { self.files.find_file(file); } }