Skip to content

Commit c503634

Browse files
authored
feat(search): an option to sort by Levenshtein distance (#140)
1 parent 6545755 commit c503634

File tree

4 files changed

+42
-9
lines changed

4 files changed

+42
-9
lines changed

src/main.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ struct Args {
3434
#[arg(long, default_value_t = true)]
3535
sort: bool,
3636

37+
/// Fancy Levenshtein sort by edit distance
38+
#[arg(long, default_value_t = false)]
39+
sort_fancy: bool,
40+
3741
/// Handlebars template of the command to execute
3842
#[arg(short, long, default_value = "ssh \"{{{name}}}\"")]
3943
template: String,
@@ -58,6 +62,7 @@ fn main() -> Result<()> {
5862
config_paths: args.config,
5963
search_filter: args.search,
6064
sort_by_name: args.sort,
65+
sort_by_levenshtein: args.sort_fancy,
6166
show_proxy_command: args.show_proxy_command,
6267
command_template: args.template,
6368
command_template_on_session_start: args.on_session_start_template,

src/searchable.rs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,36 @@
1+
use fuzzy_matcher::FuzzyMatcher;
2+
use fuzzy_matcher::skim::SkimMatcherV2;
3+
14
type SearchableFn<T> = dyn FnMut(&&T, &str) -> bool;
25

6+
pub trait SearchableItem {
7+
fn search_text(&self) -> &str;
8+
}
9+
310
pub struct Searchable<T>
411
where
5-
T: Clone,
12+
T: Clone + SearchableItem,
613
{
14+
sort_by_levenshtein: bool,
715
vec: Vec<T>,
8-
16+
matcher: SkimMatcherV2,
917
filter: Box<SearchableFn<T>>,
1018
filtered: Vec<T>,
1119
}
1220

1321
impl<T> Searchable<T>
1422
where
15-
T: Clone,
23+
T: Clone + SearchableItem,
1624
{
1725
#[must_use]
18-
pub fn new<P>(vec: Vec<T>, search_value: &str, predicate: P) -> Self
26+
pub fn new<P>(sort_by_levenshtein: bool, vec: Vec<T>, search_value: &str, predicate: P) -> Self
1927
where
2028
P: FnMut(&&T, &str) -> bool + 'static,
2129
{
2230
let mut searchable = Self {
31+
sort_by_levenshtein,
2332
vec,
24-
33+
matcher: SkimMatcherV2::default(),
2534
filter: Box::new(predicate),
2635
filtered: Vec::new(),
2736
};
@@ -35,12 +44,22 @@ where
3544
return;
3645
}
3746

38-
self.filtered = self
47+
let mut items: Vec<_> = self
3948
.vec
4049
.iter()
4150
.filter(|host| (self.filter)(host, value))
42-
.cloned()
51+
.map(|item| {
52+
let score = self.matcher.fuzzy_match(item.search_text(), value).unwrap_or(0);
53+
(item.clone(), score)
54+
})
4355
.collect();
56+
57+
// Sort by Levenshtein distance in descending order (higher score = better match)
58+
if self.sort_by_levenshtein {
59+
items.sort_by(|a, b| b.1.cmp(&a.1));
60+
}
61+
62+
self.filtered = items.into_iter().map(|(item, _)| item).collect();
4463
}
4564

4665
#[allow(clippy::must_use_candidate)]
@@ -64,7 +83,7 @@ where
6483

6584
impl<'a, T> IntoIterator for &'a Searchable<T>
6685
where
67-
T: Clone,
86+
T: Clone + SearchableItem,
6887
{
6988
type Item = &'a T;
7089
type IntoIter = std::slice::Iter<'a, T>;
@@ -76,7 +95,7 @@ where
7695

7796
impl<T> std::ops::Index<usize> for Searchable<T>
7897
where
79-
T: Clone,
98+
T: Clone + SearchableItem,
8099
{
81100
type Output = T;
82101

src/ssh.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::collections::VecDeque;
66
use std::process::Command;
77

88
use crate::ssh_config::{self, parser_error::ParseError, HostVecExt};
9+
use crate::searchable::SearchableItem;
910

1011
#[derive(Debug, Serialize, Clone)]
1112
pub struct Host {
@@ -17,6 +18,12 @@ pub struct Host {
1718
pub proxy_command: Option<String>,
1819
}
1920

21+
impl SearchableItem for Host {
22+
fn search_text(&self) -> &str {
23+
&self.name
24+
}
25+
}
26+
2027
impl Host {
2128
/// Uses the provided Handlebars template to run a command.
2229
///

src/ui.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub struct AppConfig {
3232

3333
pub search_filter: Option<String>,
3434
pub sort_by_name: bool,
35+
pub sort_by_levenshtein: bool,
3536
pub show_proxy_command: bool,
3637

3738
pub command_template: String,
@@ -103,6 +104,7 @@ impl App {
103104
palette: tailwind::BLUE,
104105

105106
hosts: Searchable::new(
107+
config.sort_by_levenshtein,
106108
hosts,
107109
&search_input,
108110
move |host: &&ssh::Host, search_value: &str| -> bool {

0 commit comments

Comments
 (0)