add fuzzy finder in branch list (#1658)

* add branch_find_popup
* capital F for fetch in branchlist, f for find
* add command info of return

closes #1350
This commit is contained in:
UG 2023-04-22 06:03:35 +09:00 committed by GitHub
parent a921ab543b
commit 3a6f292bf5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 475 additions and 44 deletions

View File

@ -2,7 +2,7 @@ use crate::{
accessors,
cmdbar::CommandBar,
components::{
event_pump, AppOption, BlameFileComponent,
event_pump, AppOption, BlameFileComponent, BranchFindPopup,
BranchListComponent, CommandBlocking, CommandInfo,
CommitComponent, CompareCommitsComponent, Component,
ConfirmComponent, CreateBranchComponent, DrawableComponent,
@ -73,6 +73,7 @@ pub struct App {
external_editor_popup: ExternalEditorComponent,
revision_files_popup: RevisionFilesPopup,
find_file_popup: FileFindPopup,
branch_find_popup: BranchFindPopup,
push_popup: PushComponent,
push_tags_popup: PushTagsComponent,
pull_popup: PullComponent,
@ -273,6 +274,11 @@ impl App {
theme.clone(),
key_config.clone(),
),
branch_find_popup: BranchFindPopup::new(
&queue,
theme.clone(),
key_config.clone(),
),
do_quit: QuitState::None,
cmdbar: RefCell::new(CommandBar::new(
theme.clone(),
@ -578,6 +584,7 @@ impl App {
self,
[
find_file_popup,
branch_find_popup,
msg,
reset,
commit,
@ -629,6 +636,7 @@ impl App {
rename_branch_popup,
revision_files_popup,
find_file_popup,
branch_find_popup,
push_popup,
push_tags_popup,
pull_popup,
@ -892,6 +900,11 @@ impl App {
flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
}
InternalEvent::OpenBranchFinder(branches) => {
self.branch_find_popup.open(branches)?;
flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
}
InternalEvent::OptionSwitched(o) => {
match o {
AppOption::StatusShowUntracked => {
@ -912,6 +925,11 @@ impl App {
flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
}
InternalEvent::BranchFinderChanged(idx) => {
self.select_branch_popup.branch_finder_update(idx)?;
flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
}
InternalEvent::OpenPopup(popup) => {
self.open_popup(popup)?;
flags

View File

@ -0,0 +1,339 @@
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 crossterm::event::Event;
use fuzzy_matcher::FuzzyMatcher;
use ratatui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Margin, Rect},
text::{Span, Spans},
widgets::{Block, Borders, Clear},
Frame,
};
use std::borrow::Cow;
pub struct BranchFindPopup {
queue: Queue,
visible: bool,
find_text: TextInputComponent,
query: Option<String>,
theme: SharedTheme,
branches: Vec<String>,
selection: usize,
selected_index: Option<usize>,
branches_filtered: Vec<(usize, Vec<usize>)>,
key_config: SharedKeyConfig,
}
impl BranchFindPopup {
///
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,
branches: Vec::new(),
branches_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.branches_filtered.clear();
if let Some(q) = &self.query {
let matcher =
fuzzy_matcher::skim::SkimMatcherV2::default();
let mut branches = self
.branches
.iter()
.enumerate()
.filter_map(|a| {
matcher
.fuzzy_indices(a.1, q)
.map(|(score, indices)| (score, a.0, indices))
})
.collect::<Vec<(_, _, _)>>();
branches.sort_by(|(score1, _, _), (score2, _, _)| {
score2.cmp(score1)
});
self.branches_filtered.extend(
branches.into_iter().map(|entry| (entry.1, entry.2)),
);
}
self.selection = 0;
self.refresh_selection();
}
fn refresh_selection(&mut self) {
let selection =
self.branches_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));
}
}
pub fn open(&mut self, branches: Vec<String>) -> 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.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.branches_filtered.len().saturating_sub(1));
if new_selection != self.selection {
self.selection = new_selection;
self.refresh_selection();
return true;
}
false
}
}
impl DrawableComponent for BranchFindPopup {
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.branches_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.branches_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 selected = self
.selected_index
.map_or(false, |index| index == *idx);
let full_text = trim_length_left(
&self.branches[*idx],
width,
);
Spans::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 BranchFindPopup {
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

@ -25,7 +25,7 @@ use asyncgit::{
},
AsyncGitNotification,
};
use crossterm::event::Event;
use crossterm::event::{Event, KeyEvent};
use ratatui::{
backend::Backend,
layout::{
@ -206,6 +206,12 @@ impl Component for BranchListComponent {
self.has_remotes,
!self.local,
));
out.push(CommandInfo::new(
strings::commands::find_branch(&self.key_config),
true,
true,
));
}
visibility_blocking(self)
}
@ -218,37 +224,11 @@ impl Component for BranchListComponent {
}
if let Event::Key(e) = ev {
if key_match(e, self.key_config.keys.exit_popup) {
self.hide();
} else if key_match(e, self.key_config.keys.move_down) {
return self
.move_selection(ScrollType::Up)
.map(Into::into);
} else if key_match(e, self.key_config.keys.move_up) {
return self
.move_selection(ScrollType::Down)
.map(Into::into);
} else if key_match(e, self.key_config.keys.page_down) {
return self
.move_selection(ScrollType::PageDown)
.map(Into::into);
} else if key_match(e, self.key_config.keys.page_up) {
return self
.move_selection(ScrollType::PageUp)
.map(Into::into);
} else if key_match(e, self.key_config.keys.home) {
return self
.move_selection(ScrollType::Home)
.map(Into::into);
} else if key_match(e, self.key_config.keys.end) {
return self
.move_selection(ScrollType::End)
.map(Into::into);
} else if key_match(e, self.key_config.keys.tab_toggle) {
self.local = !self.local;
self.check_remotes();
self.update_branches()?;
} else if key_match(e, self.key_config.keys.enter) {
if self.move_event(e)?.is_consumed() {
return Ok(EventState::Consumed);
}
if key_match(e, self.key_config.keys.enter) {
try_or_popup!(
self,
"switch branch error:",
@ -302,7 +282,7 @@ impl Component for BranchListComponent {
),
));
}
} else if key_match(e, self.key_config.keys.pull)
} else if key_match(e, self.key_config.keys.fetch)
&& !self.local && self.has_remotes
{
self.queue.push(InternalEvent::FetchRemotes);
@ -312,6 +292,14 @@ impl Component for BranchListComponent {
) {
//do not consume if its the more key
return Ok(EventState::NotConsumed);
} else if key_match(e, self.key_config.keys.branch_find) {
let branches = self
.branches
.iter()
.map(|b| b.name.clone())
.collect();
self.queue
.push(InternalEvent::OpenBranchFinder(branches));
}
}
@ -355,6 +343,41 @@ impl BranchListComponent {
}
}
fn move_event(&mut self, e: &KeyEvent) -> Result<EventState> {
if key_match(e, self.key_config.keys.exit_popup) {
self.hide();
} else if key_match(e, self.key_config.keys.move_down) {
return self
.move_selection(ScrollType::Up)
.map(Into::into);
} else if key_match(e, self.key_config.keys.move_up) {
return self
.move_selection(ScrollType::Down)
.map(Into::into);
} else if key_match(e, self.key_config.keys.page_down) {
return self
.move_selection(ScrollType::PageDown)
.map(Into::into);
} else if key_match(e, self.key_config.keys.page_up) {
return self
.move_selection(ScrollType::PageUp)
.map(Into::into);
} else if key_match(e, self.key_config.keys.home) {
return self
.move_selection(ScrollType::Home)
.map(Into::into);
} else if key_match(e, self.key_config.keys.end) {
return self
.move_selection(ScrollType::End)
.map(Into::into);
} else if key_match(e, self.key_config.keys.tab_toggle) {
self.local = !self.local;
self.check_remotes();
self.update_branches()?;
}
Ok(EventState::NotConsumed)
}
///
pub fn open(&mut self) -> Result<()> {
self.show()?;
@ -363,6 +386,16 @@ 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()?)?;
}
Ok(())
}
fn check_remotes(&mut self) {
if !self.local && self.visible {
self.has_remotes =

View File

@ -283,17 +283,16 @@ impl Component for FileFindPopup {
force_all: bool,
) -> CommandBlocking {
if self.is_visible() || force_all {
out.push(
CommandInfo::new(
strings::commands::close_popup(&self.key_config),
true,
true,
)
.order(1),
);
out.push(CommandInfo::new(
strings::commands::scroll_popup(&self.key_config),
true,
true,
));
out.push(CommandInfo::new(
strings::commands::scroll(&self.key_config),
strings::commands::close_fuzzy_finder(
&self.key_config,
),
true,
true,
));

View File

@ -1,4 +1,5 @@
mod blame_file;
mod branch_find_popup;
mod branchlist;
mod changes;
mod command;
@ -36,6 +37,7 @@ 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};

View File

@ -103,6 +103,7 @@ pub struct KeysList {
pub push: GituiKeyEvent,
pub open_file_tree: GituiKeyEvent,
pub file_find: GituiKeyEvent,
pub branch_find: GituiKeyEvent,
pub force_push: GituiKeyEvent,
pub fetch: GituiKeyEvent,
pub pull: GituiKeyEvent,
@ -191,6 +192,7 @@ impl Default for KeysList {
abort_merge: GituiKeyEvent::new(KeyCode::Char('A'), KeyModifiers::SHIFT),
open_file_tree: GituiKeyEvent::new(KeyCode::Char('F'), KeyModifiers::SHIFT),
file_find: GituiKeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()),
branch_find: GituiKeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()),
stage_unstage_item: GituiKeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
tag_annotate: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),
view_submodules: GituiKeyEvent::new(KeyCode::Char('S'), KeyModifiers::SHIFT),

View File

@ -113,8 +113,12 @@ pub enum InternalEvent {
///
OpenFileFinder(Vec<TreeFile>),
///
OpenBranchFinder(Vec<String>),
///
FileFinderChanged(Option<PathBuf>),
///
BranchFinderChanged(Option<usize>),
///
FetchRemotes,
///
OpenPopup(StackablePopupOpen),

View File

@ -450,6 +450,16 @@ pub mod commands {
CMD_GROUP_GENERAL,
)
}
pub fn find_branch(key_config: &SharedKeyConfig) -> CommandText {
CommandText::new(
format!(
"Find [{}]",
key_config.get_hint(key_config.keys.branch_find)
),
"find branch in list",
CMD_GROUP_GENERAL,
)
}
pub fn toggle_tabs_direct(
key_config: &SharedKeyConfig,
) -> CommandText {
@ -683,6 +693,19 @@ pub mod commands {
CMD_GROUP_DIFF,
)
}
pub fn close_fuzzy_finder(
key_config: &SharedKeyConfig,
) -> CommandText {
CommandText::new(
format!(
"Close [{}{}]",
key_config.get_hint(key_config.keys.exit_popup),
key_config.get_hint(key_config.keys.enter),
),
"close fuzzy finder",
CMD_GROUP_GENERAL,
)
}
pub fn close_popup(key_config: &SharedKeyConfig) -> CommandText {
CommandText::new(
format!(
@ -693,6 +716,17 @@ pub mod commands {
CMD_GROUP_GENERAL,
)
}
pub fn scroll_popup(key_config: &SharedKeyConfig) -> CommandText {
CommandText::new(
format!(
"Scroll [{}{}]",
key_config.get_hint(key_config.keys.popup_down),
key_config.get_hint(key_config.keys.popup_up),
),
"scroll up or down in popup",
CMD_GROUP_GENERAL,
)
}
pub fn close_msg(key_config: &SharedKeyConfig) -> CommandText {
CommandText::new(
format!(
@ -1555,7 +1589,7 @@ pub mod commands {
CommandText::new(
format!(
"Fetch [{}]",
key_config.get_hint(key_config.keys.pull),
key_config.get_hint(key_config.keys.fetch),
),
"fetch/prune",
CMD_GROUP_BRANCHES,