FuzzyFindPopup for general use (#1672)

* replace BranchFindPopup with FuzzyFindPopup
* replace FileFindPopup with FuzzyFindPopup
This commit is contained in:
UG 2023-06-20 19:57:36 +09:00 committed by GitHub
parent be801a336f
commit e90e8dc536
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 123 additions and 457 deletions

View file

@ -2,15 +2,16 @@ use crate::{
accessors,
cmdbar::CommandBar,
components::{
event_pump, AppOption, BlameFileComponent, BranchFindPopup,
event_pump, AppOption, BlameFileComponent,
BranchListComponent, CommandBlocking, CommandInfo,
CommitComponent, CompareCommitsComponent, Component,
ConfirmComponent, CreateBranchComponent, DrawableComponent,
ExternalEditorComponent, FetchComponent, FileFindPopup,
FileRevlogComponent, HelpComponent, InspectCommitComponent,
MsgComponent, OptionsPopupComponent, PullComponent,
PushComponent, PushTagsComponent, RenameBranchComponent,
ResetPopupComponent, RevisionFilesPopup, StashMsgComponent,
ExternalEditorComponent, FetchComponent, FileRevlogComponent,
FuzzyFindPopup, FuzzyFinderTarget, HelpComponent,
InspectCommitComponent, MsgComponent, OptionsPopupComponent,
PullComponent, PushComponent, PushTagsComponent,
RenameBranchComponent, ResetPopupComponent,
RevisionFilesPopup, StashMsgComponent,
SubmodulesListComponent, TagCommitComponent,
TagListComponent,
},
@ -45,7 +46,7 @@ use ratatui::{
};
use std::{
cell::{Cell, RefCell},
path::Path,
path::{Path, PathBuf},
rc::Rc,
};
use unicode_width::UnicodeWidthStr;
@ -72,8 +73,7 @@ pub struct App {
compare_commits_popup: CompareCommitsComponent,
external_editor_popup: ExternalEditorComponent,
revision_files_popup: RevisionFilesPopup,
find_file_popup: FileFindPopup,
branch_find_popup: BranchFindPopup,
fuzzy_find_popup: FuzzyFindPopup,
push_popup: PushComponent,
push_tags_popup: PushTagsComponent,
pull_popup: PullComponent,
@ -271,12 +271,7 @@ impl App {
theme.clone(),
key_config.clone(),
),
find_file_popup: FileFindPopup::new(
&queue,
theme.clone(),
key_config.clone(),
),
branch_find_popup: BranchFindPopup::new(
fuzzy_find_popup: FuzzyFindPopup::new(
&queue,
theme.clone(),
key_config.clone(),
@ -585,8 +580,7 @@ impl App {
accessors!(
self,
[
find_file_popup,
branch_find_popup,
fuzzy_find_popup,
msg,
reset,
commit,
@ -637,8 +631,7 @@ impl App {
create_branch_popup,
rename_branch_popup,
revision_files_popup,
find_file_popup,
branch_find_popup,
fuzzy_find_popup,
push_popup,
push_tags_popup,
pull_popup,
@ -897,13 +890,8 @@ impl App {
InternalEvent::StatusLastFileMoved => {
self.status_tab.last_file_moved()?;
}
InternalEvent::OpenFileFinder(files) => {
self.find_file_popup.open(&files)?;
flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
}
InternalEvent::OpenBranchFinder(branches) => {
self.branch_find_popup.open(branches)?;
InternalEvent::OpenFuzzyFinder(contents, target) => {
self.fuzzy_find_popup.open(contents, target)?;
flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
}
@ -921,14 +909,25 @@ impl App {
flags.insert(NeedsUpdate::ALL);
}
InternalEvent::FileFinderChanged(file) => {
self.files_tab.file_finder_update(&file);
self.revision_files_popup.file_finder_update(&file);
flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
}
InternalEvent::BranchFinderChanged(idx) => {
self.select_branch_popup.branch_finder_update(idx)?;
InternalEvent::FuzzyFinderChanged(
idx,
content,
target,
) => {
match target {
FuzzyFinderTarget::Branches => self
.select_branch_popup
.branch_finder_update(idx)?,
FuzzyFinderTarget::Files => {
self.files_tab.file_finder_update(
&PathBuf::from(content.clone()),
);
self.revision_files_popup.file_finder_update(
&PathBuf::from(content),
);
}
}
flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
}
@ -1104,7 +1103,7 @@ impl App {
res.push(CommandInfo::new(
strings::commands::find_file(&self.key_config),
!self.find_file_popup.is_visible(),
!self.fuzzy_find_popup.is_visible(),
(!self.any_popup_visible()
&& self.files_tab.is_visible())
|| self.revision_files_popup.is_visible()

View file

@ -1,7 +1,7 @@
use super::{
utils::scroll_vertical::VerticalScroll, visibility_blocking,
CommandBlocking, CommandInfo, Component, DrawableComponent,
EventState, InspectCommitOpen,
EventState, FuzzyFinderTarget, InspectCommitOpen,
};
use crate::{
components::ScrollType,
@ -298,8 +298,10 @@ impl Component for BranchListComponent {
.iter()
.map(|b| b.name.clone())
.collect();
self.queue
.push(InternalEvent::OpenBranchFinder(branches));
self.queue.push(InternalEvent::OpenFuzzyFinder(
branches,
FuzzyFinderTarget::Branches,
));
}
}
@ -386,13 +388,8 @@ impl BranchListComponent {
Ok(())
}
pub fn branch_finder_update(
&mut self,
idx: Option<usize>,
) -> Result<()> {
if let Some(idx) = idx {
self.set_selection(idx.try_into()?)?;
}
pub fn branch_finder_update(&mut self, idx: usize) -> Result<()> {
self.set_selection(idx.try_into()?)?;
Ok(())
}

View file

@ -1,349 +0,0 @@
use super::{
visibility_blocking, CommandBlocking, CommandInfo, Component,
DrawableComponent, EventState, ScrollType, TextInputComponent,
};
use crate::{
keys::{key_match, SharedKeyConfig},
queue::{InternalEvent, Queue},
string_utils::trim_length_left,
strings,
ui::{self, style::SharedTheme},
};
use anyhow::Result;
use asyncgit::sync::TreeFile;
use crossterm::event::Event;
use fuzzy_matcher::FuzzyMatcher;
use ratatui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Margin, Rect},
text::{Line, Span},
widgets::{Block, Borders, Clear},
Frame,
};
use std::borrow::Cow;
pub struct FileFindPopup {
queue: Queue,
visible: bool,
find_text: TextInputComponent,
query: Option<String>,
theme: SharedTheme,
files: Vec<TreeFile>,
selection: usize,
selected_index: Option<usize>,
files_filtered: Vec<(usize, Vec<usize>)>,
key_config: SharedKeyConfig,
}
impl FileFindPopup {
///
pub fn new(
queue: &Queue,
theme: SharedTheme,
key_config: SharedKeyConfig,
) -> Self {
let mut find_text = TextInputComponent::new(
theme.clone(),
key_config.clone(),
"",
"start typing..",
false,
);
find_text.embed();
Self {
queue: queue.clone(),
visible: false,
query: None,
find_text,
theme,
files: Vec::new(),
files_filtered: Vec::new(),
selected_index: None,
key_config,
selection: 0,
}
}
fn update_query(&mut self) {
if self.find_text.get_text().is_empty() {
self.set_query(None);
} else if self
.query
.as_ref()
.map_or(true, |q| q != self.find_text.get_text())
{
self.set_query(Some(
self.find_text.get_text().to_string(),
));
}
}
fn set_query(&mut self, query: Option<String>) {
self.query = query;
self.files_filtered.clear();
if let Some(q) = &self.query {
let matcher =
fuzzy_matcher::skim::SkimMatcherV2::default();
let mut files = self
.files
.iter()
.enumerate()
.filter_map(|a| {
a.1.path.to_str().and_then(|path| {
matcher.fuzzy_indices(path, q).map(
|(score, indices)| (score, a.0, indices),
)
})
})
.collect::<Vec<(_, _, _)>>();
files.sort_by(|(score1, _, _), (score2, _, _)| {
score2.cmp(score1)
});
self.files_filtered.extend(
files.into_iter().map(|entry| (entry.1, entry.2)),
);
}
self.selection = 0;
self.refresh_selection();
}
fn refresh_selection(&mut self) {
let selection =
self.files_filtered.get(self.selection).map(|a| a.0);
if self.selected_index != selection {
self.selected_index = selection;
let file = self
.selected_index
.and_then(|index| self.files.get(index))
.map(|f| f.path.clone());
self.queue.push(InternalEvent::FileFinderChanged(file));
}
}
pub fn open(&mut self, files: &[TreeFile]) -> Result<()> {
self.show()?;
self.find_text.show()?;
self.find_text.set_text(String::new());
self.query = None;
if self.files != *files {
self.files = files.to_owned();
}
self.update_query();
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 {
fn draw<B: Backend>(
&self,
f: &mut Frame<B>,
area: Rect,
) -> Result<()> {
if self.is_visible() {
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(
Block::default()
.borders(Borders::all())
.style(self.theme.title(true))
.title(Span::styled(
strings::POPUP_TITLE_FUZZY_FIND,
self.theme.title(true),
)),
area,
);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(1),
Constraint::Percentage(100),
]
.as_ref(),
)
.split(area.inner(&Margin {
horizontal: 1,
vertical: 1,
}));
self.find_text.draw(f, chunks[0])?;
if any_hits {
let title =
format!("Hits: {}", self.files_filtered.len());
let height = usize::from(chunks[1].height);
let width = usize::from(chunks[1].width);
let items = self
.files_filtered
.iter()
.take(height)
.map(|(idx, indicies)| {
let selected = self
.selected_index
.map_or(false, |index| index == *idx);
let full_text = trim_length_left(
self.files[*idx]
.path
.to_str()
.unwrap_or_default(),
width,
);
Line::from(
full_text
.char_indices()
.map(|(c_idx, c)| {
Span::styled(
Cow::from(c.to_string()),
self.theme.text(
selected,
indicies.contains(&c_idx),
),
)
})
.collect::<Vec<_>>(),
)
});
ui::draw_list_block(
f,
chunks[1],
Block::default()
.title(Span::styled(
title,
self.theme.title(true),
))
.borders(Borders::TOP),
items,
);
}
}
Ok(())
}
}
impl Component for FileFindPopup {
fn commands(
&self,
out: &mut Vec<CommandInfo>,
force_all: bool,
) -> CommandBlocking {
if self.is_visible() || force_all {
out.push(CommandInfo::new(
strings::commands::scroll_popup(&self.key_config),
true,
true,
));
out.push(CommandInfo::new(
strings::commands::close_fuzzy_finder(
&self.key_config,
),
true,
true,
));
}
visibility_blocking(self)
}
fn event(
&mut self,
event: &crossterm::event::Event,
) -> Result<EventState> {
if self.is_visible() {
if let Event::Key(key) = event {
if key_match(key, self.key_config.keys.exit_popup)
|| key_match(key, self.key_config.keys.enter)
{
self.hide();
} else if key_match(
key,
self.key_config.keys.popup_down,
) {
self.move_selection(ScrollType::Down);
} else if key_match(
key,
self.key_config.keys.popup_up,
) {
self.move_selection(ScrollType::Up);
}
}
if self.find_text.event(event)?.is_consumed() {
self.update_query();
}
return Ok(EventState::Consumed);
}
Ok(EventState::NotConsumed)
}
fn is_visible(&self) -> bool {
self.visible
}
fn hide(&mut self) {
self.visible = false;
}
fn show(&mut self) -> Result<()> {
self.visible = true;
Ok(())
}
}

View file

@ -1,6 +1,7 @@
use super::{
visibility_blocking, CommandBlocking, CommandInfo, Component,
DrawableComponent, EventState, ScrollType, TextInputComponent,
DrawableComponent, EventState, FuzzyFinderTarget, ScrollType,
TextInputComponent,
};
use crate::{
keys::{key_match, SharedKeyConfig},
@ -21,20 +22,21 @@ use ratatui::{
};
use std::borrow::Cow;
pub struct BranchFindPopup {
pub struct FuzzyFindPopup {
queue: Queue,
visible: bool,
find_text: TextInputComponent,
query: Option<String>,
theme: SharedTheme,
branches: Vec<String>,
contents: Vec<String>,
selection: usize,
selected_index: Option<usize>,
branches_filtered: Vec<(usize, Vec<usize>)>,
filtered: Vec<(usize, Vec<usize>)>,
key_config: SharedKeyConfig,
target: Option<FuzzyFinderTarget>,
}
impl BranchFindPopup {
impl FuzzyFindPopup {
///
pub fn new(
queue: &Queue,
@ -56,11 +58,12 @@ impl BranchFindPopup {
query: None,
find_text,
theme,
branches: Vec::new(),
branches_filtered: Vec::new(),
contents: Vec::new(),
filtered: Vec::new(),
selected_index: None,
key_config,
selection: 0,
target: None,
}
}
@ -81,14 +84,14 @@ impl BranchFindPopup {
fn set_query(&mut self, query: Option<String>) {
self.query = query;
self.branches_filtered.clear();
self.filtered.clear();
if let Some(q) = &self.query {
let matcher =
fuzzy_matcher::skim::SkimMatcherV2::default();
let mut branches = self
.branches
let mut contents = self
.contents
.iter()
.enumerate()
.filter_map(|a| {
@ -98,12 +101,12 @@ impl BranchFindPopup {
})
.collect::<Vec<(_, _, _)>>();
branches.sort_by(|(score1, _, _), (score2, _, _)| {
contents.sort_by(|(score1, _, _), (score2, _, _)| {
score2.cmp(score1)
});
self.branches_filtered.extend(
branches.into_iter().map(|entry| (entry.1, entry.2)),
self.filtered.extend(
contents.into_iter().map(|entry| (entry.1, entry.2)),
);
}
@ -113,23 +116,37 @@ impl BranchFindPopup {
fn refresh_selection(&mut self) {
let selection =
self.branches_filtered.get(self.selection).map(|a| a.0);
self.filtered.get(self.selection).map(|a| a.0);
if self.selected_index != selection {
self.selected_index = selection;
let idx = self.selected_index;
self.queue.push(InternalEvent::BranchFinderChanged(idx));
if let Some(idx) = self.selected_index {
if let Some(target) = self.target {
self.queue.push(
InternalEvent::FuzzyFinderChanged(
idx,
self.contents[idx].clone(),
target,
),
);
}
}
}
}
pub fn open(&mut self, branches: Vec<String>) -> Result<()> {
pub fn open(
&mut self,
contents: Vec<String>,
target: FuzzyFinderTarget,
) -> Result<()> {
self.show()?;
self.find_text.show()?;
self.find_text.set_text(String::new());
self.query = None;
if self.branches != branches {
self.branches = branches;
self.target = Some(target);
if self.contents != contents {
self.contents = contents;
}
self.update_query();
@ -144,7 +161,7 @@ impl BranchFindPopup {
};
let new_selection = new_selection
.clamp(0, self.branches_filtered.len().saturating_sub(1));
.clamp(0, self.filtered.len().saturating_sub(1));
if new_selection != self.selection {
self.selection = new_selection;
@ -156,7 +173,7 @@ impl BranchFindPopup {
}
}
impl DrawableComponent for BranchFindPopup {
impl DrawableComponent for FuzzyFindPopup {
fn draw<B: Backend>(
&self,
f: &mut Frame<B>,
@ -165,7 +182,7 @@ impl DrawableComponent for BranchFindPopup {
if self.is_visible() {
const MAX_SIZE: (u16, u16) = (50, 20);
let any_hits = !self.branches_filtered.is_empty();
let any_hits = !self.filtered.is_empty();
let area = ui::centered_rect_absolute(
MAX_SIZE.0, MAX_SIZE.1, area,
@ -215,22 +232,18 @@ impl DrawableComponent for BranchFindPopup {
self.find_text.draw(f, chunks[0])?;
if any_hits {
let title =
format!("Hits: {}", self.branches_filtered.len());
let title = format!("Hits: {}", self.filtered.len());
let height = usize::from(chunks[1].height);
let width = usize::from(chunks[1].width);
let items = self
.branches_filtered
.iter()
.take(height)
.map(|(idx, indicies)| {
let items = self.filtered.iter().take(height).map(
|(idx, indicies)| {
let selected = self
.selected_index
.map_or(false, |index| index == *idx);
let full_text = trim_length_left(
&self.branches[*idx],
&self.contents[*idx],
width,
);
Line::from(
@ -247,7 +260,8 @@ impl DrawableComponent for BranchFindPopup {
})
.collect::<Vec<_>>(),
)
});
},
);
ui::draw_list_block(
f,
@ -266,7 +280,7 @@ impl DrawableComponent for BranchFindPopup {
}
}
impl Component for BranchFindPopup {
impl Component for FuzzyFindPopup {
fn commands(
&self,
out: &mut Vec<CommandInfo>,

View file

@ -1,5 +1,4 @@
mod blame_file;
mod branch_find_popup;
mod branchlist;
mod changes;
mod command;
@ -12,8 +11,8 @@ mod cred;
mod diff;
mod externaleditor;
mod fetch;
mod file_find_popup;
mod file_revlog;
mod fuzzy_find_popup;
mod help;
mod inspect_commit;
mod msg;
@ -37,7 +36,6 @@ mod utils;
pub use self::status_tree::StatusTreeComponent;
pub use blame_file::{BlameFileComponent, BlameFileOpen};
pub use branch_find_popup::BranchFindPopup;
pub use branchlist::BranchListComponent;
pub use changes::ChangesComponent;
pub use command::{CommandInfo, CommandText};
@ -49,8 +47,8 @@ pub use create_branch::CreateBranchComponent;
pub use diff::DiffComponent;
pub use externaleditor::ExternalEditorComponent;
pub use fetch::FetchComponent;
pub use file_find_popup::FileFindPopup;
pub use file_revlog::{FileRevOpen, FileRevlogComponent};
pub use fuzzy_find_popup::FuzzyFindPopup;
pub use help::HelpComponent;
pub use inspect_commit::{InspectCommitComponent, InspectCommitOpen};
pub use msg::MsgComponent;
@ -236,6 +234,12 @@ pub enum EventState {
NotConsumed,
}
#[derive(Copy, Clone)]
pub enum FuzzyFinderTarget {
Branches,
Files,
}
impl EventState {
pub fn is_consumed(&self) -> bool {
*self == Self::Consumed

View file

@ -1,7 +1,7 @@
use super::{
utils::scroll_vertical::VerticalScroll, BlameFileOpen,
CommandBlocking, CommandInfo, Component, DrawableComponent,
EventState, FileRevOpen, SyntaxTextComponent,
EventState, FileRevOpen, FuzzyFinderTarget, SyntaxTextComponent,
};
use crate::{
keys::{key_match, SharedKeyConfig},
@ -30,11 +30,7 @@ use ratatui::{
Frame,
};
use std::{borrow::Cow, fmt::Write};
use std::{
collections::BTreeSet,
convert::From,
path::{Path, PathBuf},
};
use std::{collections::BTreeSet, convert::From, path::Path};
use unicode_truncate::UnicodeTruncateStr;
use unicode_width::UnicodeWidthStr;
@ -233,17 +229,26 @@ impl RevisionFilesComponent {
}
fn open_finder(&self) {
self.queue.push(InternalEvent::OpenFileFinder(
self.files.clone().unwrap_or_default(),
));
if let Some(files) = self.files.clone() {
self.queue.push(InternalEvent::OpenFuzzyFinder(
files
.iter()
.map(|a| {
a.path
.to_str()
.unwrap_or_default()
.to_string()
})
.collect(),
FuzzyFinderTarget::Files,
));
}
}
pub fn find_file(&mut self, file: &Option<PathBuf>) {
if let Some(file) = file {
self.tree.collapse_but_root();
if self.tree.select_file(file) {
self.selection_changed();
}
pub fn find_file(&mut self, file: &Path) {
self.tree.collapse_but_root();
if self.tree.select_file(file) {
self.selection_changed();
}
}

View file

@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::path::Path;
use super::{
revision_files::RevisionFilesComponent, visibility_blocking,
@ -91,7 +91,7 @@ impl RevisionFilesPopup {
self.files.any_work_pending()
}
pub fn file_finder_update(&mut self, file: &Option<PathBuf>) {
pub fn file_finder_update(&mut self, file: &Path) {
self.files.find_file(file);
}

View file

@ -1,12 +1,12 @@
use crate::{
components::{
AppOption, BlameFileOpen, FileRevOpen, FileTreeOpen,
InspectCommitOpen,
FuzzyFinderTarget, InspectCommitOpen,
},
tabs::StashingOptions,
};
use asyncgit::{
sync::{diff::DiffLinePosition, CommitId, TreeFile},
sync::{diff::DiffLinePosition, CommitId},
PushType,
};
use bitflags::bitflags;
@ -111,13 +111,9 @@ pub enum InternalEvent {
///
OptionSwitched(AppOption),
///
OpenFileFinder(Vec<TreeFile>),
OpenFuzzyFinder(Vec<String>, FuzzyFinderTarget),
///
OpenBranchFinder(Vec<String>),
///
FileFinderChanged(Option<PathBuf>),
///
BranchFinderChanged(Option<usize>),
FuzzyFinderChanged(usize, String, FuzzyFinderTarget),
///
FetchRemotes,
///

View file

@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::path::Path;
use crate::{
components::{
@ -75,7 +75,7 @@ impl FilesTab {
Ok(())
}
pub fn file_finder_update(&mut self, file: &Option<PathBuf>) {
pub fn file_finder_update(&mut self, file: &Path) {
self.files.find_file(file);
}
}