use dawkey_studio::PluginInfo;
use dawkey_ui::Modifiers;
use crate::App;
use crate::view::plugins::{PICKER_TOP, ROW_H};
const MAX_QUERY: usize = 60;
pub(crate) fn fuzzy_score(term: &str, text: &str) -> Option<i32> {
let text: Vec<char> = text.to_lowercase().chars().collect();
let term: Vec<char> = term.chars().collect();
let word_start = |i: usize| i == 0 || !text[i - 1].is_alphanumeric();
let n = term.len() as i32;
if term.is_empty() {
return Some(0);
}
if let Some(at) = text.windows(term.len()).position(|w| w == term) {
let whole = text.len() == term.len();
return Some(
40 * n + 40 + if word_start(at) { 30 } else { 0 } + if whole { 30 } else { 0 }
- (at as i32).min(20),
);
}
const NONE: i32 = i32::MIN / 2;
let letter = |j: usize| 10 + if word_start(j) { 15 } else { 0 };
let mut best: Vec<i32> = (0..text.len())
.map(|j| match text[j] == term[0] {
true => letter(j) - (j as i32).min(20),
false => NONE,
})
.collect();
for &c in &term[1..] {
let mut next = vec![NONE; text.len()];
let mut far = NONE;
for j in 0..text.len() {
if j >= 11 {
far = far.max(best[j - 11]);
}
if text[j] != c {
continue;
}
let near = (j.saturating_sub(10)..j).map(|k| match j - k - 1 {
0 => best[k] + 15,
gap => best[k] - gap as i32,
});
next[j] = near.fold(far - 10, i32::max) + letter(j);
}
best = next;
}
best.into_iter().max().filter(|&s| s > NONE / 2)
}
fn plugin_score(terms: &[String], p: &PluginInfo) -> Option<i32> {
let format = p.id.split(':').next().unwrap_or("");
let kind = if p.instrument { "instrument" } else { "effect" };
let rest = format!("{} {format} {kind}", p.vendor);
terms
.iter()
.map(|t| {
fuzzy_score(t, &p.name)
.map(|s| s * 2)
.max(fuzzy_score(t, &rest))
})
.sum()
}
pub(crate) fn picker_matches<'a>(
plugins: &'a [PluginInfo],
instrument: Option<bool>,
query: &str,
) -> Vec<&'a PluginInfo> {
let terms: Vec<String> = query.split_whitespace().map(str::to_lowercase).collect();
let mut found: Vec<(i32, &PluginInfo)> = plugins
.iter()
.filter(|p| instrument.is_none_or(|i| p.instrument == i))
.filter_map(|p| Some((plugin_score(&terms, p)?, p)))
.collect();
found.sort_by_key(|&(score, _)| std::cmp::Reverse(score));
found.into_iter().map(|(_, p)| p).collect()
}
impl App {
pub(crate) fn picker_ids(&self) -> Vec<String> {
self.studio.cached_catalog().map_or_else(Vec::new, |c| {
picker_matches(&c.plugins, self.view.picker_filter, &self.view.picker_query)
.into_iter()
.map(|p| p.id.clone())
.collect()
})
}
pub(crate) fn on_picker_key(&mut self, key: &str, m: Modifiers) -> Option<Result<(), String>> {
self.view.plugin_picker?;
if m.ctrl {
return None;
}
let count = self.picker_ids().len();
let view = &mut self.view;
match key {
"escape" if view.picker_query.is_empty() => self.close_overlays(),
"escape" => self.set_picker_query(String::new()),
"return" if count > 0 => return Some(self.add_picked(self.view.picker_selected)),
"return" => {}
"up" | "down" | "home" | "end" => {
let last = count.saturating_sub(1);
view.picker_selected = match key {
"up" => view.picker_selected.saturating_sub(1),
"down" => (view.picker_selected + 1).min(last),
"home" => 0,
_ => last,
};
self.scroll_to_picked();
}
"backspace" | "delete" => {
let mut query = view.picker_query.clone();
query.pop();
self.set_picker_query(query);
}
_ => {
let c = match key {
"space" => ' ',
_ => match key.chars().collect::<Vec<_>>()[..] {
[c] if !c.is_control() => c,
_ => return None,
},
};
let c = if m.shift { c.to_ascii_uppercase() } else { c };
let mut query = view.picker_query.clone();
if query.chars().count() < MAX_QUERY {
query.push(c);
}
self.set_picker_query(query);
}
}
Some(Ok(()))
}
fn set_picker_query(&mut self, query: String) {
self.view.picker_query = query;
self.view.picker_selected = 0;
self.view.overlay_scroll = 0.0;
}
fn scroll_to_picked(&mut self) {
let visible = self
.ui
.find("plugin_picker/list")
.map_or(400.0, |n| n.rect().h);
let top = PICKER_TOP + self.view.picker_selected as f32 * ROW_H;
let scroll = &mut self.view.overlay_scroll;
if top - PICKER_TOP < *scroll {
*scroll = top - PICKER_TOP;
} else if top + ROW_H + PICKER_TOP > *scroll + visible {
*scroll = top + ROW_H + PICKER_TOP - visible;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn substrings_beat_scattered_letters() {
let reverb = fuzzy_score("verb", "Dragonfly Hall Reverb").unwrap();
let scattered = fuzzy_score("verb", "Vintage Equalizer Rack Bus").unwrap();
assert!(reverb > scattered, "{reverb} vs {scattered}");
assert!(fuzzy_score("comp", "LSP Compressor") > fuzzy_score("comp", "Decompose"));
assert!(fuzzy_score("gc", "Graphic Compressor") > fuzzy_score("gc", "Rage Cog"));
assert_eq!(fuzzy_score("vreb", "Reverb"), None);
assert_eq!(fuzzy_score("x", "Reverb"), None);
assert_eq!(fuzzy_score("", "Reverb"), Some(0));
assert!(fuzzy_score("über", "Überdrive").is_some());
}
}