Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Searching The Result List #29

Merged
merged 3 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
highlight searches instead of filtering the list
  • Loading branch information
yassinebridi committed Aug 2, 2024
commit 5d17c1cf363861c0de88e636c3ae08ff2af8f541
122 changes: 87 additions & 35 deletions src/components/search_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ pub struct SearchResult {
match_counts: Vec<String>,
search_input: Input,
is_searching: bool,
debounce_timer: Option<tokio::task::JoinHandle<()>>,
search_matches: Vec<usize>,
current_match_index: usize,
}

impl SearchResult {
Expand Down Expand Up @@ -215,43 +216,75 @@ impl SearchResult {
(KeyCode::Char('r'), _) => {
self.replace_single_file(state);
},
(KeyCode::Esc, _) => {
self.is_searching = false;
self.search_matches.clear();
self.search_input.reset();
self.current_match_index = 0;
},
(KeyCode::Enter, _) => {
let action = AppAction::Action(Action::SetActiveTab { tab: Tab::Preview });
self.command_tx.as_ref().unwrap().send(action).unwrap();
},
(KeyCode::Char('n'), _) => {
self.next_match(state);
},
(KeyCode::Char('p'), _) => {
self.previous_match(state);
},
_ => {},
}
}

// Filter the search result list based on the search input
fn toggle_search(&mut self) {
self.is_searching = !self.is_searching;
}

fn handle_search_input(&mut self, key: KeyEvent, state: &State) {
match (key.code, key.modifiers) {
(KeyCode::Esc, _) => {
match key.code {
KeyCode::Esc | KeyCode::Enter => {
self.is_searching = false;
},
_ => {
self.search_input.handle_event(&crossterm::event::Event::Key(key));
self.debounce_search();
self.perform_search(state);
},
}
}

fn debounce_search(&mut self) {
if let Some(timer) = self.debounce_timer.take() {
timer.abort();
fn perform_search(&mut self, state: &State) {
let search_term = self.search_input.value().to_lowercase();
self.search_matches.clear();
self.current_match_index = 0;

for (index, result) in state.search_result.list.iter().enumerate() {
if result.path.to_lowercase().contains(&search_term) {
let result_index = result.index.unwrap();
self.search_matches.push(result_index);
}
}
log::info!("111Search matches: {:?}", self.search_matches);

let search_term = self.search_input.value().to_string();
let tx = self.command_tx.clone().unwrap();
if !self.search_matches.is_empty() {
self.state.select(Some(self.search_matches[0]));
self.update_selected_result(state);
}
}

fn next_match(&mut self, state: &State) {
log::info!("Next match");
log::info!("Search matches: {:?}", self.search_matches);
if !self.search_matches.is_empty() {
self.current_match_index = (self.current_match_index + 1) % self.search_matches.len();
let next_index = self.search_matches[self.current_match_index];
self.state.select(Some(next_index));
self.update_selected_result(state);
}
}

self.debounce_timer = Some(tokio::spawn(async move {
tokio::time::sleep(DEBOUNCE_DURATION).await;
tx.send(AppAction::Action(Action::UpdateSearchResultFilter(search_term))).unwrap();
}));
fn previous_match(&mut self, state: &State) {
if !self.search_matches.is_empty() {
self.current_match_index = (self.current_match_index + self.search_matches.len() - 1) % self.search_matches.len();
let prev_index = self.search_matches[self.current_match_index];
self.state.select(Some(prev_index));
self.update_selected_result(state);
}
}
}

Expand All @@ -268,20 +301,20 @@ impl Component for SearchResult {

fn handle_key_events(&mut self, key: KeyEvent, state: &State) -> Result<Option<AppAction>> {
if state.focused_screen == FocusedScreen::SearchResultList {
match (key.code, key.modifiers) {
(KeyCode::Char('/'), _) => {
self.toggle_search();
match key.code {
KeyCode::Char('/') => {
self.is_searching = true;
self.search_input.reset();
Ok(None)
},
_ if self.is_searching => {
self.handle_search_input(key, state);
Ok(None)
},
_ if !self.is_searching => {
_ => {
self.handle_local_key_events(key, state);
Ok(None)
},
_ => Ok(None),
}
} else {
Ok(None)
Expand All @@ -300,24 +333,43 @@ impl Component for SearchResult {
};

let project_root = state.project_root.to_string_lossy();
let results_to_display = if self.is_searching { &state.filtered_search_result } else { &state.search_result.list };
let results_to_display = &state.search_result.list;
let search_term = self.search_input.value().to_lowercase();

let list_items: Vec<ListItem> = results_to_display
.iter()
.map(|s| {
let text = Line::from(vec![
Span::raw(s.path.strip_prefix(format!("{}/", project_root).as_str()).unwrap_or(&s.path)),
Span::raw(" ("),
Span::styled(s.total_matches.to_string(), Style::default().fg(Color::Yellow)),
Span::raw(")"),
]);
ListItem::new(text)
.enumerate()
.map(|(index, s)| {
let path = s.path.strip_prefix(format!("{}/", project_root).as_str()).unwrap_or(&s.path);
let mut spans = Vec::new();
let mut start = 0;

if !search_term.is_empty() {
for (idx, _) in path.to_lowercase().match_indices(&search_term) {
if start < idx {
spans.push(Span::raw(&path[start..idx]));
}
spans.push(Span::styled(
&path[idx..idx + search_term.len()],
Style::default().bg(Color::Yellow).fg(Color::Black),
));
start = idx + search_term.len();
}
}

if start < path.len() {
spans.push(Span::raw(&path[start..]));
}

spans.push(Span::raw(" ("));
spans.push(Span::styled(s.total_matches.to_string(), Style::default().fg(Color::Yellow)));
spans.push(Span::raw(")"));

ListItem::new(Line::from(spans))
})
.collect();

let internal_selected = state.selected_result.index.unwrap_or(0);
self.state.select(Some(internal_selected));
self.set_selected_result(state);
let internal_selected = self.state.selected().unwrap_or(0);

let details_widget = List::new(list_items)
.style(Style::default().fg(Color::White))
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ fn check_dependency(command: &str) -> bool {
}

async fn tokio_main() -> Result<()> {
// let _ = simple_logging::log_to_file("serpl.log", LevelFilter::Info);
let _ = simple_logging::log_to_file("serpl.log", LevelFilter::Info);

if !check_dependency("rg") {
eprintln!("\x1b[31mError: ripgrep (rg) is not installed. Please install it to use serpl.\x1b[0m");
Expand Down
1 change: 0 additions & 1 deletion src/redux/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,4 @@ pub enum Action {
SetFocusedScreen { screen: Option<FocusedScreen> },
RemoveFileFromList { index: usize },
RemoveLineFromFile { file_index: usize, line_index: usize },
UpdateSearchResultFilter(String),
}
22 changes: 1 addition & 21 deletions src/redux/reducer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ use crate::{

pub fn reducer(state: State, action: Action) -> State {
match action {
Action::SetSearchList { search_list } => {
let filtered_search_result = search_list.list.clone();
State { search_result: search_list, filtered_search_result, ..state }
},
Action::SetSearchList { search_list } => State { search_result: search_list, ..state },
Action::SetSelectedResult { result } => State { selected_result: result, ..state },
Action::SetSearchText { text } => {
let is_dialog_visible = check_dialog_visible(&state);
Expand Down Expand Up @@ -172,23 +169,6 @@ pub fn reducer(state: State, action: Action) -> State {
state
}
},
Action::UpdateSearchResultFilter(filter) => {
log::info!("Filtering search results with filter: {}", filter);
let filtered_list = if filter.is_empty() {
log::info!("Filter is empty, returning original list");
state.search_result.list.clone()
} else {
state
.search_result
.list
.iter()
.filter(|result| result.path.to_lowercase().contains(&filter.to_lowercase()))
.cloned()
.collect()
};

State { search_result_filter: filter, filtered_search_result: filtered_list, ..state }
},
}
}

Expand Down
2 changes: 0 additions & 2 deletions src/redux/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ pub struct State {
pub previous_focused_screen: FocusedScreen,
pub help_dialog_visible: bool,
pub is_large_folder: bool,
pub search_result_filter: String,
pub filtered_search_result: Vec<SearchResultState>,
}

#[derive(Default, Clone, PartialEq, Eq, Debug)]
Expand Down