Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/view/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ impl<'a> ListView<'a> {
selected,
height,
scroll_to_top,
search_context,
} = list_context;
let list_state = self.as_mut_list_state();
list_state.reset_height(*height);
Expand All @@ -276,5 +277,8 @@ impl<'a> ListView<'a> {
list_state.scroll_up();
}
}
if let Some(search_context) = search_context {
list_state.restore_search(search_context);
}
}
}
5 changes: 4 additions & 1 deletion src/view/views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
detail::DetailView, help::HelpView, list::ListView, refs::RefsView,
user_command::UserCommandView,
},
widget::commit_list::CommitListState,
widget::commit_list::{CommitListState, SearchRefreshContext},
};

#[derive(Debug, Default)]
Expand Down Expand Up @@ -193,6 +193,7 @@ pub struct ListRefreshViewContext {
pub selected: usize,
pub height: usize,
pub scroll_to_top: bool,
pub search_context: Option<SearchRefreshContext>,
}

impl From<&CommitListState<'_>> for ListRefreshViewContext {
Expand All @@ -202,11 +203,13 @@ impl From<&CommitListState<'_>> for ListRefreshViewContext {
// If the selected commit is the top one and there is no offset, it means the list is already scrolled to the top.
// In this case, we set scroll_to_top to true to indicate that the view should be scrolled to the top after refresh.
let scroll_to_top = selected == 0 && offset == 0;
let search_context = list_state.search_refresh_context();
ListRefreshViewContext {
commit_hash,
selected,
height,
scroll_to_top,
search_context,
}
}
}
Expand Down
207 changes: 206 additions & 1 deletion src/widget/commit_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,18 @@ pub enum SearchState {
Applied {
match_index: usize,
total_match: usize,
ignore_case: bool,
fuzzy: bool,
},
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SearchRefreshContext {
query: String,
ignore_case: bool,
fuzzy: bool,
}

impl SearchState {
fn update_match_index(&mut self, index: usize) {
match self {
Expand Down Expand Up @@ -503,19 +512,64 @@ impl<'a> CommitListState<'a> {
}

pub fn apply_search(&mut self) {
if let SearchState::Searching { match_index, .. } = self.search_state {
if let SearchState::Searching {
match_index,
ignore_case,
fuzzy,
..
} = self.search_state
{
if self.search_input.value().is_empty() {
self.search_state = SearchState::Inactive;
} else {
let total_match = self.search_matches.iter().filter(|m| m.matched()).count();
self.search_state = SearchState::Applied {
match_index,
total_match,
ignore_case,
fuzzy,
};
}
}
}

pub fn search_refresh_context(&self) -> Option<SearchRefreshContext> {
if let SearchState::Applied {
ignore_case, fuzzy, ..
} = self.search_state
{
Some(SearchRefreshContext {
query: self.search_input.value().into(),
ignore_case,
fuzzy,
})
} else {
None
}
}

pub fn restore_search(&mut self, context: &SearchRefreshContext) {
self.search_input = Input::new(context.query.clone());
self.update_search_matches(context.ignore_case, context.fuzzy);

let total_match = self.search_matches.iter().filter(|m| m.matched()).count();
self.search_state = SearchState::Applied {
// The selected commit may not match after refresh; next/previous updates this value.
match_index: 0,
total_match,
ignore_case: context.ignore_case,
fuzzy: context.fuzzy,
};

if total_match > 0 {
let current_index = self.current_selected_index();
if self.search_matches[current_index].matched() {
self.search_state
.update_match_index(self.search_matches[current_index].match_index);
}
}
}

pub fn cancel_search(&mut self) {
if let SearchState::Searching { .. } | SearchState::Applied { .. } = self.search_state {
self.search_state = SearchState::Inactive;
Expand Down Expand Up @@ -1155,8 +1209,159 @@ fn calc_cell_widths(

#[cfg(test)]
mod tests {
use std::path::PathBuf;

use ratatui::crossterm::event::KeyCode;

use crate::{
color::GraphColorSet,
config::GraphColorConfig,
git::Repository,
graph::{calc_graph, CellWidthType, GraphImageWidthMode, GraphStyle},
protocol::ImageProtocol,
};

use super::*;

fn with_commit_list_state<R>(
subjects: &[&str],
f: impl FnOnce(&mut CommitListState<'_>) -> R,
) -> R {
let commits: Vec<Commit> = subjects
.iter()
.enumerate()
.map(|(i, subject)| Commit {
commit_hash: CommitHash::from(format!("{:040x}", i + 1).as_str()),
subject: (*subject).into(),
..Commit::default()
})
.collect();
let commit_hashes = commits.iter().map(|c| c.commit_hash.clone()).collect();
let commit_map = commits
.into_iter()
.map(|c| (c.commit_hash.clone(), c))
.collect();
let repository = Repository::new(
PathBuf::new(),
commit_map,
FxHashMap::default(),
FxHashMap::default(),
FxHashMap::default(),
Head::None,
commit_hashes,
);
let graph = calc_graph(&repository);
let graph_color_set = GraphColorSet::new(&GraphColorConfig::default());
let graph_image_manager = GraphImageManager::new(
&graph,
&graph_color_set,
CellWidthType::Double,
GraphStyle::Rounded,
GraphImageWidthMode::Compact,
ImageProtocol::Iterm2,
);
let commit_infos = graph
.commits
.iter()
.map(|commit| {
CommitInfo::new(commit, repository.refs(&commit.commit_hash), Color::Reset)
})
.collect();
let mut state = CommitListState::new(
commit_infos,
graph_image_manager,
0,
repository.head(),
FxHashMap::default(),
false,
false,
);
state.reset_height(subjects.len());
f(&mut state)
}

fn input_search_query(state: &mut CommitListState<'_>, query: &str) {
state.start_search();
for c in query.chars() {
state.handle_search_input(KeyEvent::from(KeyCode::Char(c)));
}
}

#[test]
fn test_restore_search_recalculates_matches_with_applied_options() {
let context = with_commit_list_state(&["Fix parser", "other"], |state| {
input_search_query(state, "fx");
state.toggle_ignore_case();
state.toggle_fuzzy();
state.apply_search();

state.search_refresh_context().unwrap()
});

assert_eq!(
context,
SearchRefreshContext {
query: "fx".into(),
ignore_case: true,
fuzzy: true,
}
);

with_commit_list_state(&["unrelated", "FIX new", "fix parser"], |state| {
state.restore_search(&context);

assert_eq!(state.search_refresh_context(), Some(context.clone()));
assert_eq!(
state.commits[state.current_selected_index()].commit.subject,
"unrelated"
);

state.select_next_match();
assert_eq!(
state.matched_query_string(),
Some(("Match 1 of 2 (query: \"fx\")".into(), true))
);
assert_eq!(
state.commits[state.current_selected_index()].commit.subject,
"FIX new"
);

state.select_next_match();
assert_eq!(
state.matched_query_string(),
Some(("Match 2 of 2 (query: \"fx\")".into(), true))
);
assert_eq!(
state.commits[state.current_selected_index()].commit.subject,
"fix parser"
);
});
}

#[test]
fn test_restore_search_keeps_selected_match_position() {
let context = with_commit_list_state(&["fix"], |state| {
input_search_query(state, "fix");
state.apply_search();
state.search_refresh_context().unwrap()
});

with_commit_list_state(&["first", "second", "fix", "last"], |state| {
state.reset_height(2);
state.select_index(2);
state.scroll_up();
assert_eq!(state.current_list_status(), (1, 1, 2));

state.restore_search(&context);

assert_eq!(state.current_list_status(), (1, 1, 2));
assert_eq!(
state.matched_query_string(),
Some(("Match 1 of 1 (query: \"fix\")".into(), true))
);
});
}

#[test]
fn test_calc_cell_widths_all_columns() {
let area_width = 80;
Expand Down