diff --git a/src/ui/components/emote_picker.rs b/src/ui/components/emote_picker.rs index a48c69dd..8cb48b04 100644 --- a/src/ui/components/emote_picker.rs +++ b/src/ui/components/emote_picker.rs @@ -1,5 +1,6 @@ +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use log::warn; -use memchr::memmem; +use once_cell::sync::Lazy; use std::cmp::max; use tui::{ layout::Rect, @@ -32,6 +33,8 @@ use crate::{ }, }; +static FUZZY_FINDER: Lazy = Lazy::new(SkimMatcherV2::default); + pub struct EmotePickerWidget { config: SharedCompleteConfig, emotes: SharedEmotes, @@ -129,7 +132,7 @@ impl Component for EmotePickerWidget { let mut items = Vec::with_capacity(max_len); let mut bad_emotes = vec![]; - let mut current_input = self.input.to_string(); + let current_input = self.input.to_string(); let cell_size = *self .emotes @@ -137,69 +140,74 @@ impl Component for EmotePickerWidget { .get() .expect("Terminal cell size should be set when emotes are enabled."); - let finder = if current_input.is_empty() { - None - } else { - current_input.make_ascii_lowercase(); - Some(memmem::Finder::new(¤t_input)) - }; - - for (name, (filename, zero_width)) in self - .emotes - .user_emotes - .borrow() - .iter() - .chain(self.emotes.global_emotes.borrow().iter()) + // Enter a new scope to drop the user/global emotes borrow when we don't need them anymore. { - if items.len() >= max_len { - break; - } + let user_emotes = self.emotes.user_emotes.borrow(); + let global_emotes = self.emotes.global_emotes.borrow(); + + // First find all the emotes that match the input + let mut matched_emotes = user_emotes + .iter() + .chain(global_emotes.iter()) + .filter_map(|(name, data)| { + Some(( + name, + data, + FUZZY_FINDER.fuzzy_indices(&name.to_ascii_lowercase(), ¤t_input)?, + )) + }) + .collect::>(); + + // Sort them by match score + matched_emotes.sort_by(|a, b| b.2 .0.cmp(&a.2 .0)); + + for (name, (filename, zero_width), (_, matched_indices)) in matched_emotes { + if items.len() >= max_len { + break; + } - // Skip emotes that do not contain the current input, if it is not empty. - let Some(pos) = finder - .as_ref() - .map_or_else(|| Some(0), |f| f.find(name.to_ascii_lowercase().as_bytes())) - else { - continue; - }; - - let Ok(loaded_emote) = load_picker_emote( - name, - filename, - *zero_width, - &mut self.emotes.info.borrow_mut(), - cell_size, - ) - .map_err(|e| warn!("{e}")) else { - bad_emotes.push(name.clone()); - continue; - }; - - let cols = (loaded_emote.width as f32 / cell_size.0).ceil() as u16; - - #[cfg(not(target_os = "windows"))] - let underline_style = Style::default() - .fg(u32_to_color(loaded_emote.hash)) - .underline_color(u32_to_color(1)); - - #[cfg(target_os = "windows")] - let underline_style = { Style::default().fg(u32_to_color(loaded_emote.hash)) }; - - let row = vec![ - Span::raw(name[0..pos].to_owned()), - Span::styled( - name[pos..(pos + current_input.len())].to_owned(), - self.search_theme, - ), - Span::raw(name[(pos + current_input.len())..].to_owned()), - Span::raw(" - "), - Span::styled( + let Ok(loaded_emote) = load_picker_emote( + name, + filename, + *zero_width, + &mut self.emotes.info.borrow_mut(), + cell_size, + ) + .map_err(|e| warn!("{e}")) else { + bad_emotes.push(name.clone()); + continue; + }; + + let cols = (loaded_emote.width as f32 / cell_size.0).ceil() as u16; + + #[cfg(not(target_os = "windows"))] + let underline_style = Style::default() + .fg(u32_to_color(loaded_emote.hash)) + .underline_color(u32_to_color(1)); + + #[cfg(target_os = "windows")] + let underline_style = { Style::default().fg(u32_to_color(loaded_emote.hash)) }; + + let mut row = name + .chars() + .enumerate() + .map(|(i, c)| { + if matched_indices.contains(&i) { + Span::styled(c.to_string(), self.search_theme) + } else { + Span::raw(c.to_string()) + } + }) + .collect::>(); + + row.push(Span::raw(" - ")); + row.push(Span::styled( UnicodePlaceholder::new(cols as usize).string(), underline_style, - ), - ]; + )); - items.push((name.clone(), ListItem::new(vec![Line::from(row)]))); + items.push((name.clone(), ListItem::new(vec![Line::from(row)]))); + } } // Remove emotes that could not be loaded from list of emotes