summaryrefslogtreecommitdiffstats
path: root/src/tabs
diff options
context:
space:
mode:
authorextrawurst <776816+extrawurst@users.noreply.github.com>2023-08-18 17:19:18 +0200
committerGitHub <noreply@github.com>2023-08-18 17:19:18 +0200
commit3c5131ad27592b892a7af33d6fd22ffcd14fe7e4 (patch)
tree6c22f43014387fa22767a5fbd579d91178f54a10 /src/tabs
parentb50d44a4b82304d1c77329bdf4230d229bd06022 (diff)
commit log filtering (#1800)
Diffstat (limited to 'src/tabs')
-rw-r--r--src/tabs/revlog.rs209
-rw-r--r--src/tabs/stashlist.rs2
-rw-r--r--src/tabs/status.rs6
3 files changed, 202 insertions, 15 deletions
diff --git a/src/tabs/revlog.rs b/src/tabs/revlog.rs
index 73ecef0d..05590c46 100644
--- a/src/tabs/revlog.rs
+++ b/src/tabs/revlog.rs
@@ -7,13 +7,17 @@ use crate::{
},
keys::{key_match, SharedKeyConfig},
queue::{InternalEvent, Queue, StackablePopupOpen},
- strings, try_or_popup,
- ui::style::SharedTheme,
+ strings::{self, order},
+ try_or_popup,
+ ui::style::{SharedTheme, Theme},
};
use anyhow::Result;
use asyncgit::{
asyncjob::AsyncSingleJob,
- sync::{self, CommitId, RepoPathRef},
+ sync::{
+ self, filter_commit_by_search, CommitId, LogFilterSearch,
+ LogFilterSearchOptions, RepoPathRef,
+ },
AsyncBranchesJob, AsyncGitNotification, AsyncLog, AsyncTags,
CommitFilesParams, FetchStatus,
};
@@ -21,26 +25,44 @@ use crossbeam_channel::Sender;
use crossterm::event::Event;
use ratatui::{
backend::Backend,
- layout::{Constraint, Direction, Layout, Rect},
+ layout::{Alignment, Constraint, Direction, Layout, Rect},
+ text::Span,
+ widgets::{Block, Borders, Paragraph},
Frame,
};
-use std::time::Duration;
+use std::{collections::HashSet, rc::Rc, time::Duration};
use sync::CommitTags;
const SLICE_SIZE: usize = 1200;
+struct LogSearchResult {
+ commits: Vec<CommitId>,
+ options: LogFilterSearchOptions,
+ duration: Duration,
+}
+
+//TODO: deserves its on component
+enum LogSearch {
+ Off,
+ Searching(AsyncLog, LogFilterSearchOptions),
+ Results(LogSearchResult),
+}
+
///
pub struct Revlog {
repo: RepoPathRef,
commit_details: CommitDetailsComponent,
list: CommitList,
git_log: AsyncLog,
+ search: LogSearch,
git_tags: AsyncTags,
git_local_branches: AsyncSingleJob<AsyncBranchesJob>,
git_remote_branches: AsyncSingleJob<AsyncBranchesJob>,
queue: Queue,
visible: bool,
key_config: SharedKeyConfig,
+ sender: Sender<AsyncGitNotification>,
+ theme: SharedTheme,
}
impl Revlog {
@@ -65,7 +87,7 @@ impl Revlog {
list: CommitList::new(
repo.clone(),
&strings::log_title(&key_config),
- theme,
+ theme.clone(),
queue.clone(),
key_config.clone(),
),
@@ -74,34 +96,45 @@ impl Revlog {
sender,
None,
),
+ search: LogSearch::Off,
git_tags: AsyncTags::new(repo.borrow().clone(), sender),
git_local_branches: AsyncSingleJob::new(sender.clone()),
git_remote_branches: AsyncSingleJob::new(sender.clone()),
visible: false,
key_config,
+ sender: sender.clone(),
+ theme,
}
}
///
pub fn any_work_pending(&self) -> bool {
self.git_log.is_pending()
+ || self.is_search_pending()
|| self.git_tags.is_pending()
|| self.git_local_branches.is_pending()
|| self.git_remote_branches.is_pending()
|| self.commit_details.any_work_pending()
}
+ const fn is_search_pending(&self) -> bool {
+ matches!(self.search, LogSearch::Searching(_, _))
+ }
+
///
pub fn update(&mut self) -> Result<()> {
if self.is_visible() {
let log_changed =
self.git_log.fetch()? == FetchStatus::Started;
+ let search_changed = self.update_search_state()?;
+ let log_changed = log_changed || search_changed;
+
self.list.set_count_total(self.git_log.count()?);
let selection = self.list.selection();
let selection_max = self.list.selection_max();
- if self.list.items().needs_data(selection, selection_max)
+ if self.list.needs_data(selection, selection_max)
|| log_changed
{
self.fetch_commits()?;
@@ -184,7 +217,8 @@ impl Revlog {
);
if let Ok(commits) = commits {
- self.list.items().set_items(want_min, commits);
+ let highlighted = self.search_result_set();
+ self.list.set_items(want_min, commits, &highlighted);
}
Ok(())
@@ -236,6 +270,117 @@ impl Revlog {
));
}
}
+
+ pub fn search(
+ &mut self,
+ options: LogFilterSearchOptions,
+ ) -> Result<()> {
+ if matches!(
+ self.search,
+ LogSearch::Off | LogSearch::Results(_)
+ ) {
+ log::info!("start search: {:?}", options);
+
+ let filter = filter_commit_by_search(
+ LogFilterSearch::new(options.clone()),
+ );
+
+ let mut async_find = AsyncLog::new(
+ self.repo.borrow().clone(),
+ &self.sender,
+ Some(filter),
+ );
+
+ async_find.fetch()?;
+
+ self.search = LogSearch::Searching(async_find, options);
+
+ self.fetch_commits()?;
+ }
+
+ Ok(())
+ }
+
+ fn search_result_set(&self) -> Option<HashSet<CommitId>> {
+ if let LogSearch::Results(results) = &self.search {
+ Some(
+ results
+ .commits
+ .iter()
+ .map(CommitId::clone)
+ .collect::<HashSet<_>>(),
+ )
+ } else {
+ None
+ }
+ }
+
+ fn update_search_state(&mut self) -> Result<bool> {
+ let changes = match &self.search {
+ LogSearch::Off | LogSearch::Results(_) => false,
+ LogSearch::Searching(search, options) => {
+ if search.is_pending() {
+ false
+ } else {
+ let results = search.get_items()?;
+ let duration = search.get_last_duration()?;
+ self.search =
+ LogSearch::Results(LogSearchResult {
+ commits: results,
+ options: options.clone(),
+ duration,
+ });
+ true
+ }
+ }
+ };
+
+ Ok(changes)
+ }
+
+ fn is_in_search_mode(&self) -> bool {
+ !matches!(self.search, LogSearch::Off)
+ }
+
+ //TODO: draw time a search took
+ fn draw_search<B: Backend>(&self, f: &mut Frame<B>, area: Rect) {
+ let text = match &self.search {
+ LogSearch::Searching(_, options) => {
+ format!(
+ "'{}' (pending results...)",
+ options.search_pattern.clone()
+ )
+ }
+ LogSearch::Results(results) => {
+ format!(
+ "'{}' (hits: {}) (duration: {:?})",
+ results.options.search_pattern.clone(),
+ results.commits.len(),
+ results.duration,
+ )
+ }
+ LogSearch::Off => String::new(),
+ };
+
+ f.render_widget(
+ Paragraph::new(text)
+ .block(
+ Block::default()
+ .title(Span::styled(
+ strings::POPUP_TITLE_LOG_SEARCH,
+ self.theme.title(true),
+ ))
+ .borders(Borders::ALL)
+ .border_style(Theme::attention_block()),
+ )
+ .alignment(Alignment::Left),
+ area,
+ );
+ }
+
+ fn can_leave_search(&self) -> bool {
+ self.is_in_search_mode() && !self.is_search_pending()
+ }
}
impl DrawableComponent for Revlog {
@@ -244,6 +389,18 @@ impl DrawableComponent for Revlog {
f: &mut Frame<B>,
area: Rect,
) -> Result<()> {
+ let area = if self.is_in_search_mode() {
+ Layout::default()
+ .direction(Direction::Vertical)
+ .constraints(
+ [Constraint::Min(1), Constraint::Length(3)]
+ .as_ref(),
+ )
+ .split(area)
+ } else {
+ Rc::new([area])
+ };
+
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(
@@ -253,13 +410,17 @@ impl DrawableComponent for Revlog {
]
.as_ref(),
)
- .split(area);
+ .split(area[0]);
if self.commit_details.is_visible() {
self.list.draw(f, chunks[0])?;
self.commit_details.draw(f, chunks[1])?;
} else {
- self.list.draw(f, area)?;
+ self.list.draw(f, area[0])?;
+ }
+
+ if self.is_in_search_mode() {
+ self.draw_search(f, area[1]);
}
Ok(())
@@ -281,6 +442,15 @@ impl Component for Revlog {
self.commit_details.toggle_visible()?;
self.update()?;
return Ok(EventState::Consumed);
+ } else if key_match(
+ k,
+ self.key_config.keys.exit_popup,
+ ) {
+ if self.can_leave_search() {
+ self.search = LogSearch::Off;
+ self.fetch_commits()?;
+ return Ok(EventState::Consumed);
+ }
} else if key_match(k, self.key_config.keys.copy) {
try_or_popup!(
self,
@@ -373,6 +543,11 @@ impl Component for Revlog {
Ok(EventState::Consumed)
},
);
+ } else if key_match(k, self.key_config.keys.log_find)
+ {
+ self.queue
+ .push(InternalEvent::OpenLogSearchPopup);
+ return Ok(EventState::Consumed);
} else if key_match(
k,
self.key_config.keys.compare_commits,
@@ -418,6 +593,16 @@ impl Component for Revlog {
self.list.commands(out, force_all);
}
+ out.push(
+ CommandInfo::new(
+ strings::commands::log_close_search(&self.key_config),
+ true,
+ (self.visible && self.can_leave_search())
+ || force_all,
+ )
+ .order(order::PRIORITY),
+ );
+
out.push(CommandInfo::new(
strings::commands::log_details_toggle(&self.key_config),
true,
@@ -516,6 +701,10 @@ impl Component for Revlog {
fn hide(&mut self) {
self.visible = false;
self.git_log.set_background();
+ //TODO:
+ // self.git_log_find
+ // .as_mut()
+ // .map(asyncgit::AsyncLog::set_background);
}
fn show(&mut self) -> Result<()> {
diff --git a/src/tabs/stashlist.rs b/src/tabs/stashlist.rs
index c2eb1d86..1ac691b0 100644
--- a/src/tabs/stashlist.rs
+++ b/src/tabs/stashlist.rs
@@ -55,7 +55,7 @@ impl StashList {
)?;
self.list.set_count_total(commits.len());
- self.list.items().set_items(0, commits);
+ self.list.set_items(0, commits, &None);
}
Ok(())
diff --git a/src/tabs/status.rs b/src/tabs/status.rs
index ec49cdc1..f59ca940 100644
--- a/src/tabs/status.rs
+++ b/src/tabs/status.rs
@@ -10,7 +10,7 @@ use crate::{
options::SharedOptions,
queue::{Action, InternalEvent, NeedsUpdate, Queue, ResetItem},
strings, try_or_popup,
- ui::style::SharedTheme,
+ ui::style::{SharedTheme, Theme},
};
use anyhow::Result;
use asyncgit::{
@@ -313,9 +313,7 @@ impl Status {
Block::default()
.border_type(BorderType::Plain)
.borders(Borders::all())
- .border_style(
- Style::default().fg(Color::Yellow),
- )
+ .border_style(Theme::attention_block())
.title(format!(
"Pending {:?}",
self.git_state