diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index 531ad95..aded90a 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -7,6 +7,8 @@ use crate::tui::{ lib::IMusicHoard, }; +use super::selection::IncSearch; + pub enum AppState { Browse(BS), Info(IS), @@ -293,19 +295,19 @@ impl IAppInteractReloadPrivate for App { impl IAppInteractSearch for App { fn append_character(&mut self, ch: char) { - let s = self.state.as_mut().unwrap_search(); - s.push(ch); + let collection = self.music_hoard.get_collection(); + let search = self.state.as_mut().unwrap_search(); + search.push(ch); self.selection - .incremental_artist_search(self.music_hoard.get_collection(), s); + .incremental_artist_search(IncSearch::Forward, collection, search); } fn remove_character(&mut self) { - let s = self.state.as_mut().unwrap_search(); - s.pop(); + let collection = self.music_hoard.get_collection(); + let search = self.state.as_mut().unwrap_search(); + search.pop(); self.selection - .reset_artist(self.music_hoard.get_collection()); - self.selection - .incremental_artist_search(self.music_hoard.get_collection(), s); + .incremental_artist_search(IncSearch::Reverse, collection, search); } fn finish_search(&mut self) { diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index 147cd50..b1afdb2 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -61,6 +61,11 @@ impl Delta { } } +pub enum IncSearch { + Forward, + Reverse, +} + impl Selection { pub fn new(artists: &[Artist]) -> Self { Selection { @@ -95,8 +100,14 @@ impl Selection { }; } - pub fn incremental_artist_search(&mut self, collection: &Collection, artist_name: &str) { - self.artist.incremental_search(collection, artist_name); + pub fn incremental_artist_search( + &mut self, + direction: IncSearch, + collection: &Collection, + artist_name: &str, + ) { + self.artist + .incremental_search(direction, collection, artist_name); } pub fn increment_selection(&mut self, collection: &Collection, delta: Delta) { @@ -192,7 +203,8 @@ impl ArtistSelection { } } - fn normalize_search_string(search: &str, lowercase: bool) -> String { + // FIXME: use aho_corasick for normalization + fn normalize_search_string(search: &str, lowercase: bool, asciify: bool) -> String { let normalized = if lowercase { search.to_lowercase() } else { @@ -202,40 +214,121 @@ impl ArtistSelection { // Unlikely that this covers all possible strings, but it should at least cover strings // relevant for music (at least in English). The list of characters handled is based on // https://wiki.musicbrainz.org/User:Yurim/Punctuation_and_Special_Characters. - normalized - .replace("‐", "-") // U+2010 hyphen - .replace("‒", "-") // U+2012 figure dash - .replace("–", "-") // U+2013 en dash - .replace("—", "-") // U+2014 em dash - .replace("―", "-") // U+2015 horizontal bar - .replace("‘", "'") // U+2018 - .replace("’", "'") // U+2019 - .replace("“", "\"") // U+201C - .replace("”", "\"") // U+201D - .replace("…", "...") // U+2026 - .replace("−", "-") // U+2212 minus sign + if asciify { + normalized + .replace("‐", "-") // U+2010 hyphen + .replace("‒", "-") // U+2012 figure dash + .replace("–", "-") // U+2013 en dash + .replace("—", "-") // U+2014 em dash + .replace("―", "-") // U+2015 horizontal bar + .replace("‘", "'") // U+2018 + .replace("’", "'") // U+2019 + .replace("“", "\"") // U+201C + .replace("”", "\"") // U+201D + .replace("…", "...") // U+2026 + .replace("−", "-") // U+2212 minus sign + } else { + normalized + } } - fn incremental_search(&mut self, artists: &[Artist], artist_name: &str) { - if let Some(index) = self.state.list.selected() { - let case_sensitive = artist_name - .chars() - .any(|ch| ch.is_alphabetic() && ch.is_uppercase()); - let search_name = Self::normalize_search_string(artist_name, !case_sensitive); - let slice = &artists[index..]; + fn is_case_sensitive(artist_name: &str) -> bool { + artist_name + .chars() + .any(|ch| ch.is_alphabetic() && ch.is_uppercase()) + } - let result = slice.binary_search_by(|probe| { - Self::normalize_search_string(&probe.get_sort_key().name, !case_sensitive) - .cmp(&search_name) - }); + fn is_char_sensitive(artist_name: &str) -> bool { + let special_chars: &[char] = &['‐', '‒', '–', '—', '―', '‘', '’', '“', '”', '…', '−']; + artist_name.chars().any(|ch| special_chars.contains(&ch)) + } - let new_index = match result { - Ok(slice_index) | Err(slice_index) => index + slice_index, - }; - self.select_to(artists, new_index); + // FIXME: compare against both sort key and actual name + fn incremental_search_predicate( + case_sensitive: bool, + char_sensitive: bool, + search_name: &String, + probe: &Artist, + ) -> bool { + let probe_name = &probe.get_sort_key().name; + match Self::normalize_search_string(probe_name, !case_sensitive, !char_sensitive) + .cmp(search_name) + { + std::cmp::Ordering::Less => false, + std::cmp::Ordering::Equal | std::cmp::Ordering::Greater => true, } } + fn incremental_search(&mut self, direction: IncSearch, artists: &[Artist], artist_name: &str) { + if let Some(index) = self.state.list.selected() { + let case_sensitive = Self::is_case_sensitive(artist_name); + let char_sensitive = Self::is_char_sensitive(artist_name); + let search_name = + Self::normalize_search_string(artist_name, !case_sensitive, !char_sensitive); + + match direction { + IncSearch::Forward => self.forward_incremental_search( + artists, + index, + case_sensitive, + char_sensitive, + &search_name, + ), + IncSearch::Reverse => self.reverse_incremental_search( + artists, + index, + case_sensitive, + char_sensitive, + &search_name, + ), + } + } + } + + fn forward_incremental_search( + &mut self, + artists: &[Artist], + index: usize, + case_sensitive: bool, + char_sensitive: bool, + search: &String, + ) { + let slice = &artists[index..]; + + let result = slice.iter().position(|probe| { + Self::incremental_search_predicate(case_sensitive, char_sensitive, &search, probe) + }); + + let new_index = match result { + Some(slice_index) => index + slice_index, + None => artists.len(), + }; + self.select_to(artists, new_index); + } + + fn reverse_incremental_search( + &mut self, + artists: &[Artist], + index: usize, + case_sensitive: bool, + char_sensitive: bool, + search: &String, + ) { + let slice = &artists[..(index + 1)]; + + // We search using opposite predicate in the reverse direction because what matters is the + // point at which the predicate flips value. + let result = slice.iter().rev().position(|probe| { + !Self::incremental_search_predicate(case_sensitive, char_sensitive, &search, probe) + }); + + let new_index = match result { + Some(slice_index) => index - slice_index + 1, + None => 0, + }; + self.select_to(artists, new_index); + } + fn increment_by(&mut self, artists: &[Artist], by: usize) { if let Some(index) = self.state.list.selected() { let result = index.saturating_add(by); diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 5a0d967..8860ad3 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -58,6 +58,7 @@ impl IEventHandlerPrivate for EventHandler { // Exit application on `Ctrl-C`. KeyCode::Char('c') | KeyCode::Char('C') => { app.force_quit(); + return; } _ => {} }