mirror of
https://github.com/gitui-org/gitui
synced 2026-05-24 01:18:21 +00:00
644 lines
13 KiB
Rust
644 lines
13 KiB
Rust
use super::{
|
|
visibility_blocking, CommandBlocking, CommandInfo, Component,
|
|
DrawableComponent, EventState, TextInputComponent,
|
|
};
|
|
use crate::{
|
|
keys::{key_match, SharedKeyConfig},
|
|
queue::{InternalEvent, Queue},
|
|
strings::{self, POPUP_COMMIT_SHA_INVALID},
|
|
ui::{self, style::SharedTheme},
|
|
};
|
|
use anyhow::Result;
|
|
use asyncgit::sync::{
|
|
CommitId, LogFilterSearchOptions, RepoPathRef, SearchFields,
|
|
SearchOptions,
|
|
};
|
|
use crossterm::event::Event;
|
|
use easy_cast::Cast;
|
|
use ratatui::{
|
|
backend::Backend,
|
|
layout::{
|
|
Alignment, Constraint, Direction, Layout, Margin, Rect,
|
|
},
|
|
text::{Line, Span},
|
|
widgets::{Block, Borders, Clear, Paragraph},
|
|
Frame,
|
|
};
|
|
|
|
enum Selection {
|
|
EnterText,
|
|
FuzzyOption,
|
|
CaseOption,
|
|
SummarySearch,
|
|
MessageBodySearch,
|
|
FilenameSearch,
|
|
AuthorsSearch,
|
|
}
|
|
|
|
enum PopupMode {
|
|
Search,
|
|
JumpCommitSha,
|
|
}
|
|
|
|
pub struct LogSearchPopupComponent {
|
|
repo: RepoPathRef,
|
|
queue: Queue,
|
|
visible: bool,
|
|
mode: PopupMode,
|
|
selection: Selection,
|
|
key_config: SharedKeyConfig,
|
|
find_text: TextInputComponent,
|
|
options: (SearchFields, SearchOptions),
|
|
theme: SharedTheme,
|
|
jump_commit_id: Option<CommitId>,
|
|
}
|
|
|
|
impl LogSearchPopupComponent {
|
|
///
|
|
pub fn new(
|
|
repo: RepoPathRef,
|
|
queue: &Queue,
|
|
theme: SharedTheme,
|
|
key_config: SharedKeyConfig,
|
|
) -> Self {
|
|
let mut find_text = TextInputComponent::new(
|
|
theme.clone(),
|
|
key_config.clone(),
|
|
"",
|
|
"search text",
|
|
false,
|
|
);
|
|
find_text.embed();
|
|
find_text.enabled(true);
|
|
|
|
Self {
|
|
repo,
|
|
queue: queue.clone(),
|
|
visible: false,
|
|
mode: PopupMode::Search,
|
|
key_config,
|
|
options: (
|
|
SearchFields::default(),
|
|
SearchOptions::default(),
|
|
),
|
|
theme,
|
|
find_text,
|
|
selection: Selection::EnterText,
|
|
jump_commit_id: None,
|
|
}
|
|
}
|
|
|
|
pub fn open(&mut self) -> Result<()> {
|
|
self.show()?;
|
|
self.selection = Selection::EnterText;
|
|
self.find_text.show()?;
|
|
self.find_text.set_text(String::new());
|
|
self.find_text.enabled(true);
|
|
|
|
self.set_mode(&PopupMode::Search);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn set_mode(&mut self, mode: &PopupMode) {
|
|
self.find_text.set_text(String::new());
|
|
|
|
match mode {
|
|
PopupMode::Search => {
|
|
self.mode = PopupMode::Search;
|
|
self.find_text.set_default_msg("search text".into());
|
|
self.find_text.enabled(matches!(
|
|
self.selection,
|
|
Selection::EnterText
|
|
));
|
|
}
|
|
PopupMode::JumpCommitSha => {
|
|
self.mode = PopupMode::JumpCommitSha;
|
|
self.jump_commit_id = None;
|
|
self.find_text.set_default_msg("commit sha".into());
|
|
self.find_text.enabled(false);
|
|
self.selection = Selection::EnterText;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn execute_confirm(&mut self) {
|
|
self.hide();
|
|
|
|
if !self.is_valid() {
|
|
return;
|
|
}
|
|
|
|
match self.mode {
|
|
PopupMode::Search => {
|
|
self.queue.push(InternalEvent::CommitSearch(
|
|
LogFilterSearchOptions {
|
|
fields: self.options.0,
|
|
options: self.options.1,
|
|
search_pattern: self
|
|
.find_text
|
|
.get_text()
|
|
.to_string(),
|
|
},
|
|
));
|
|
}
|
|
PopupMode::JumpCommitSha => {
|
|
let commit_id = self.jump_commit_id
|
|
.expect("Commit id must have value here because it's already validated");
|
|
self.queue.push(InternalEvent::SelectCommitInRevlog(
|
|
commit_id,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_valid(&self) -> bool {
|
|
match self.mode {
|
|
PopupMode::Search => {
|
|
!self.find_text.get_text().trim().is_empty()
|
|
}
|
|
PopupMode::JumpCommitSha => self.jump_commit_id.is_some(),
|
|
}
|
|
}
|
|
|
|
fn validate_commit_sha(&mut self) {
|
|
let path = self.repo.borrow();
|
|
if let Ok(commit_id) = CommitId::from_revision(
|
|
&path,
|
|
self.find_text.get_text().trim(),
|
|
) {
|
|
self.jump_commit_id = Some(commit_id);
|
|
} else {
|
|
self.jump_commit_id = None;
|
|
}
|
|
}
|
|
|
|
fn get_text_options(&self) -> Vec<Line> {
|
|
let x_summary =
|
|
if self.options.0.contains(SearchFields::MESSAGE_SUMMARY)
|
|
{
|
|
"X"
|
|
} else {
|
|
" "
|
|
};
|
|
|
|
let x_body =
|
|
if self.options.0.contains(SearchFields::MESSAGE_BODY) {
|
|
"X"
|
|
} else {
|
|
" "
|
|
};
|
|
|
|
let x_files =
|
|
if self.options.0.contains(SearchFields::FILENAMES) {
|
|
"X"
|
|
} else {
|
|
" "
|
|
};
|
|
|
|
let x_authors =
|
|
if self.options.0.contains(SearchFields::AUTHORS) {
|
|
"X"
|
|
} else {
|
|
" "
|
|
};
|
|
|
|
let x_opt_fuzzy =
|
|
if self.options.1.contains(SearchOptions::FUZZY_SEARCH) {
|
|
"X"
|
|
} else {
|
|
" "
|
|
};
|
|
|
|
let x_opt_casesensitive =
|
|
if self.options.1.contains(SearchOptions::CASE_SENSITIVE)
|
|
{
|
|
"X"
|
|
} else {
|
|
" "
|
|
};
|
|
|
|
vec![
|
|
Line::from(vec![Span::styled(
|
|
format!("[{x_opt_fuzzy}] fuzzy search"),
|
|
self.theme.text(
|
|
matches!(self.selection, Selection::FuzzyOption),
|
|
false,
|
|
),
|
|
)]),
|
|
Line::from(vec![Span::styled(
|
|
format!("[{x_opt_casesensitive}] case sensitive"),
|
|
self.theme.text(
|
|
matches!(self.selection, Selection::CaseOption),
|
|
false,
|
|
),
|
|
)]),
|
|
Line::from(vec![Span::styled(
|
|
format!("[{x_summary}] summary",),
|
|
self.theme.text(
|
|
matches!(
|
|
self.selection,
|
|
Selection::SummarySearch
|
|
),
|
|
false,
|
|
),
|
|
)]),
|
|
Line::from(vec![Span::styled(
|
|
format!("[{x_body}] message body",),
|
|
self.theme.text(
|
|
matches!(
|
|
self.selection,
|
|
Selection::MessageBodySearch
|
|
),
|
|
false,
|
|
),
|
|
)]),
|
|
Line::from(vec![Span::styled(
|
|
format!("[{x_files}] commited files",),
|
|
self.theme.text(
|
|
matches!(
|
|
self.selection,
|
|
Selection::FilenameSearch
|
|
),
|
|
false,
|
|
),
|
|
)]),
|
|
Line::from(vec![Span::styled(
|
|
format!("[{x_authors}] authors",),
|
|
self.theme.text(
|
|
matches!(
|
|
self.selection,
|
|
Selection::AuthorsSearch
|
|
),
|
|
false,
|
|
),
|
|
)]),
|
|
]
|
|
}
|
|
|
|
fn option_selected(&self) -> bool {
|
|
!matches!(self.selection, Selection::EnterText)
|
|
}
|
|
|
|
fn toggle_option(&mut self) {
|
|
match self.selection {
|
|
Selection::EnterText => (),
|
|
Selection::FuzzyOption => {
|
|
self.options.1.toggle(SearchOptions::FUZZY_SEARCH);
|
|
}
|
|
Selection::CaseOption => {
|
|
self.options.1.toggle(SearchOptions::CASE_SENSITIVE);
|
|
}
|
|
Selection::SummarySearch => {
|
|
self.options.0.toggle(SearchFields::MESSAGE_SUMMARY);
|
|
|
|
if self.options.0.is_empty() {
|
|
self.options
|
|
.0
|
|
.set(SearchFields::MESSAGE_BODY, true);
|
|
}
|
|
}
|
|
Selection::MessageBodySearch => {
|
|
self.options.0.toggle(SearchFields::MESSAGE_BODY);
|
|
|
|
if self.options.0.is_empty() {
|
|
self.options.0.set(SearchFields::FILENAMES, true);
|
|
}
|
|
}
|
|
Selection::FilenameSearch => {
|
|
self.options.0.toggle(SearchFields::FILENAMES);
|
|
|
|
if self.options.0.is_empty() {
|
|
self.options.0.set(SearchFields::AUTHORS, true);
|
|
}
|
|
}
|
|
Selection::AuthorsSearch => {
|
|
self.options.0.toggle(SearchFields::AUTHORS);
|
|
|
|
if self.options.0.is_empty() {
|
|
self.options
|
|
.0
|
|
.set(SearchFields::MESSAGE_SUMMARY, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn move_selection(&mut self, arg: bool) {
|
|
if arg {
|
|
//up
|
|
self.selection = match self.selection {
|
|
Selection::EnterText => Selection::AuthorsSearch,
|
|
Selection::FuzzyOption => Selection::EnterText,
|
|
Selection::CaseOption => Selection::FuzzyOption,
|
|
Selection::SummarySearch => Selection::CaseOption,
|
|
Selection::MessageBodySearch => {
|
|
Selection::SummarySearch
|
|
}
|
|
Selection::FilenameSearch => {
|
|
Selection::MessageBodySearch
|
|
}
|
|
Selection::AuthorsSearch => Selection::FilenameSearch,
|
|
};
|
|
} else {
|
|
self.selection = match self.selection {
|
|
Selection::EnterText => Selection::FuzzyOption,
|
|
Selection::FuzzyOption => Selection::CaseOption,
|
|
Selection::CaseOption => Selection::SummarySearch,
|
|
Selection::SummarySearch => {
|
|
Selection::MessageBodySearch
|
|
}
|
|
Selection::MessageBodySearch => {
|
|
Selection::FilenameSearch
|
|
}
|
|
Selection::FilenameSearch => Selection::AuthorsSearch,
|
|
Selection::AuthorsSearch => Selection::EnterText,
|
|
};
|
|
}
|
|
|
|
self.find_text
|
|
.enabled(matches!(self.selection, Selection::EnterText));
|
|
}
|
|
|
|
fn draw_search_mode<B: Backend>(
|
|
&self,
|
|
f: &mut Frame<B>,
|
|
area: Rect,
|
|
) -> Result<()> {
|
|
const SIZE: (u16, u16) = (60, 10);
|
|
let area = ui::centered_rect_absolute(SIZE.0, SIZE.1, area);
|
|
|
|
f.render_widget(Clear, area);
|
|
f.render_widget(
|
|
Block::default()
|
|
.borders(Borders::all())
|
|
.style(self.theme.title(true))
|
|
.title(Span::styled(
|
|
strings::POPUP_TITLE_LOG_SEARCH,
|
|
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])?;
|
|
|
|
f.render_widget(
|
|
Paragraph::new(self.get_text_options())
|
|
.block(
|
|
Block::default()
|
|
.borders(Borders::TOP)
|
|
.border_style(self.theme.block(true)),
|
|
)
|
|
.alignment(Alignment::Left),
|
|
chunks[1],
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn draw_commit_sha_mode<B: Backend>(
|
|
&self,
|
|
f: &mut Frame<B>,
|
|
area: Rect,
|
|
) -> Result<()> {
|
|
const SIZE: (u16, u16) = (60, 3);
|
|
let area = ui::centered_rect_absolute(SIZE.0, SIZE.1, area);
|
|
|
|
let mut block_style = self.theme.title(true);
|
|
|
|
let show_invalid = !self.is_valid()
|
|
&& !self.find_text.get_text().trim().is_empty();
|
|
|
|
if show_invalid {
|
|
block_style = block_style.patch(self.theme.text_danger());
|
|
}
|
|
|
|
f.render_widget(Clear, area);
|
|
f.render_widget(
|
|
Block::default()
|
|
.borders(Borders::all())
|
|
.style(block_style)
|
|
.title(Span::styled(
|
|
strings::POPUP_TITLE_LOG_SEARCH,
|
|
self.theme.title(true),
|
|
)),
|
|
area,
|
|
);
|
|
|
|
let chunks = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints([Constraint::Length(1)].as_ref())
|
|
.split(area.inner(&Margin {
|
|
horizontal: 1,
|
|
vertical: 1,
|
|
}));
|
|
|
|
self.find_text.draw(f, chunks[0])?;
|
|
|
|
if show_invalid {
|
|
self.draw_invalid_sha(f);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn draw_invalid_sha<B: Backend>(&self, f: &mut Frame<B>) {
|
|
let msg_length: u16 = POPUP_COMMIT_SHA_INVALID.len().cast();
|
|
let w = Paragraph::new(POPUP_COMMIT_SHA_INVALID)
|
|
.style(self.theme.text_danger());
|
|
|
|
let rect = {
|
|
let mut rect = self.find_text.get_area();
|
|
rect.y += rect.height;
|
|
rect.height = 1;
|
|
let offset = rect.width.saturating_sub(msg_length);
|
|
rect.width = rect.width.saturating_sub(offset);
|
|
rect.x += offset;
|
|
|
|
rect
|
|
};
|
|
|
|
f.render_widget(w, rect);
|
|
}
|
|
|
|
#[inline]
|
|
fn event_search_mode(
|
|
&mut self,
|
|
event: &crossterm::event::Event,
|
|
) -> Result<EventState> {
|
|
if let Event::Key(key) = &event {
|
|
if key_match(key, self.key_config.keys.exit_popup) {
|
|
self.hide();
|
|
} else if key_match(key, self.key_config.keys.enter)
|
|
&& self.is_valid()
|
|
{
|
|
self.execute_confirm();
|
|
} else if key_match(key, self.key_config.keys.popup_up) {
|
|
self.move_selection(true);
|
|
} else if key_match(
|
|
key,
|
|
self.key_config.keys.find_commit_sha,
|
|
) {
|
|
self.set_mode(&PopupMode::JumpCommitSha);
|
|
} else if key_match(key, self.key_config.keys.popup_down)
|
|
{
|
|
self.move_selection(false);
|
|
} else if key_match(
|
|
key,
|
|
self.key_config.keys.log_mark_commit,
|
|
) && self.option_selected()
|
|
{
|
|
self.toggle_option();
|
|
} else if !self.option_selected() {
|
|
self.find_text.event(event)?;
|
|
}
|
|
}
|
|
|
|
Ok(EventState::Consumed)
|
|
}
|
|
|
|
#[inline]
|
|
fn event_commit_sha_mode(
|
|
&mut self,
|
|
event: &crossterm::event::Event,
|
|
) -> Result<EventState> {
|
|
if let Event::Key(key) = &event {
|
|
if key_match(key, self.key_config.keys.exit_popup) {
|
|
self.set_mode(&PopupMode::Search);
|
|
} else if key_match(key, self.key_config.keys.enter)
|
|
&& self.is_valid()
|
|
{
|
|
self.execute_confirm();
|
|
} else if self.find_text.event(event)?.is_consumed() {
|
|
self.validate_commit_sha();
|
|
self.find_text.enabled(
|
|
!self.find_text.get_text().trim().is_empty(),
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(EventState::Consumed)
|
|
}
|
|
}
|
|
|
|
impl DrawableComponent for LogSearchPopupComponent {
|
|
fn draw<B: Backend>(
|
|
&self,
|
|
f: &mut Frame<B>,
|
|
area: Rect,
|
|
) -> Result<()> {
|
|
if self.is_visible() {
|
|
match self.mode {
|
|
PopupMode::Search => {
|
|
self.draw_search_mode(f, area)?;
|
|
}
|
|
PopupMode::JumpCommitSha => {
|
|
self.draw_commit_sha_mode(f, area)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Component for LogSearchPopupComponent {
|
|
fn commands(
|
|
&self,
|
|
out: &mut Vec<CommandInfo>,
|
|
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),
|
|
);
|
|
|
|
if matches!(self.mode, PopupMode::Search) {
|
|
out.push(
|
|
CommandInfo::new(
|
|
strings::commands::scroll_popup(
|
|
&self.key_config,
|
|
),
|
|
true,
|
|
true,
|
|
)
|
|
.order(1),
|
|
);
|
|
out.push(
|
|
CommandInfo::new(
|
|
strings::commands::toggle_option(
|
|
&self.key_config,
|
|
),
|
|
self.option_selected(),
|
|
true,
|
|
)
|
|
.order(1),
|
|
);
|
|
out.push(
|
|
CommandInfo::new(
|
|
strings::commands::find_commit_sha(
|
|
&self.key_config,
|
|
),
|
|
true,
|
|
true,
|
|
)
|
|
.order(1),
|
|
);
|
|
}
|
|
|
|
out.push(CommandInfo::new(
|
|
strings::commands::confirm_action(&self.key_config),
|
|
self.is_valid(),
|
|
self.visible,
|
|
));
|
|
}
|
|
|
|
visibility_blocking(self)
|
|
}
|
|
|
|
fn event(
|
|
&mut self,
|
|
event: &crossterm::event::Event,
|
|
) -> Result<EventState> {
|
|
if !self.is_visible() {
|
|
return Ok(EventState::NotConsumed);
|
|
}
|
|
|
|
match self.mode {
|
|
PopupMode::Search => self.event_search_mode(event),
|
|
PopupMode::JumpCommitSha => {
|
|
self.event_commit_sha_mode(event)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_visible(&self) -> bool {
|
|
self.visible
|
|
}
|
|
|
|
fn hide(&mut self) {
|
|
self.visible = false;
|
|
}
|
|
|
|
fn show(&mut self) -> Result<()> {
|
|
self.visible = true;
|
|
|
|
Ok(())
|
|
}
|
|
}
|