Skip to content

Commit 6fca97e

Browse files
authored
Fuzzy searching options (#763)
Addresses #759
1 parent 2b14da0 commit 6fca97e

6 files changed

Lines changed: 180 additions & 1 deletion

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,11 @@ When you're presented with suggestions, you can type to fuzzily search through t
387387

388388
[![Fuzzy path suggestions demo](https://github.com/HalFrgrd/flyline/releases/download/assets/demo_fuzzy_suggestions.gif)](https://github.com/HalFrgrd/evp)
389389

390+
You can customize the fuzzy matching behavior using the `flyline suggestions set-fuzzy-mode` command. It accepts three options:
391+
- `all` (default): Fuzzy matching is enabled for all completion suggestions.
392+
- `none`: Fuzzy matching is disabled entirely (falling back to case-insensitive prefix matching).
393+
- `folder-prefixes`: Folders are matched using case-insensitive prefix matching while remaining enabled for regular files and other suggestions.
394+
390395
### Fuzzy path completion
391396
The last path segments from the cursor to the end will be fuzzily matched on the directory contents:
392397

src/active_suggestions.rs

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@ impl SuggestionDescription {
143143
}
144144
}
145145

146+
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
147+
pub enum SuggestionType {
148+
Folder,
149+
RegularFile,
150+
Misc,
151+
}
152+
146153
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
147154
pub struct ProcessedSuggestion {
148155
pub s: String,
@@ -152,6 +159,8 @@ pub struct ProcessedSuggestion {
152159
pub style: Option<Style>,
153160
/// Description to display as a visual suffix (not inserted).
154161
pub description: SuggestionDescription,
162+
/// The type of suggestion (Folder, RegularFile, or Misc).
163+
pub sug_type: SuggestionType,
155164
}
156165

157166
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -599,6 +608,7 @@ mod description_tests {
599608
std::time::Duration::from_millis(0),
600609
true, // auto_started
601610
crate::settings::SuggestionSortOrder::default(),
611+
crate::settings::FuzzyMode::default(),
602612
);
603613

604614
// Grid width/rows logic, let's call into_list with max_rows = 2
@@ -669,6 +679,7 @@ mod description_tests {
669679
std::time::Duration::from_millis(0),
670680
true, // auto_started
671681
crate::settings::SuggestionSortOrder::default(),
682+
crate::settings::FuzzyMode::default(),
672683
);
673684

674685
// Initially, in auto_started, no selection is active.
@@ -706,6 +717,7 @@ mod description_tests {
706717
std::time::Duration::from_millis(0),
707718
true, // auto_started
708719
crate::settings::SuggestionSortOrder::default(),
720+
crate::settings::FuzzyMode::default(),
709721
);
710722
assert_eq!(active_no_sel.selected_coord, None);
711723
active_no_sel.update_word_under_cursor(&SubString::new("cl", "cl").unwrap());
@@ -746,6 +758,7 @@ mod description_tests {
746758
std::time::Duration::from_millis(0),
747759
false, // auto_started
748760
crate::settings::SuggestionSortOrder::Mtime,
761+
crate::settings::FuzzyMode::default(),
749762
);
750763

751764
let filtered_names: Vec<String> = active
@@ -783,6 +796,7 @@ mod description_tests {
783796
std::time::Duration::from_millis(0),
784797
false,
785798
crate::settings::SuggestionSortOrder::Alphabetical,
799+
crate::settings::FuzzyMode::default(),
786800
);
787801

788802
let filtered_names_alpha: Vec<String> = active_alpha
@@ -821,6 +835,7 @@ mod description_tests {
821835
std::time::Duration::from_millis(0),
822836
false,
823837
crate::settings::SuggestionSortOrder::Alphabetical,
838+
crate::settings::FuzzyMode::default(),
824839
);
825840

826841
let filtered_names: Vec<String> = active
@@ -879,6 +894,7 @@ mod description_tests {
879894
std::time::Duration::from_millis(0),
880895
false,
881896
crate::settings::SuggestionSortOrder::Alphabetical,
897+
crate::settings::FuzzyMode::default(),
882898
);
883899
assert!(!active_boundary.nosort);
884900

@@ -904,6 +920,7 @@ mod description_tests {
904920
std::time::Duration::from_millis(0),
905921
false,
906922
crate::settings::SuggestionSortOrder::Alphabetical,
923+
crate::settings::FuzzyMode::default(),
907924
);
908925
assert!(active_large.nosort);
909926
}
@@ -951,13 +968,68 @@ mod description_tests {
951968
auto_started: false,
952969
nosort: false,
953970
sort_order: crate::settings::SuggestionSortOrder::default(),
971+
fuzzy_mode: crate::settings::FuzzyMode::default(),
954972
formatted_cache: vec![None, None, None],
955973
max_width_cache: std::cell::Cell::new(None),
956974
};
957975

958976
suggestions.accept_all_filtered_items(&mut buffer);
959977
assert_eq!(buffer.buffer(), "mycmd foo bar baz ");
960978
}
979+
980+
#[test]
981+
fn test_fuzzy_mode_no_folders() {
982+
let builder = ActiveSuggestionsBuilder {
983+
processed: vec![
984+
ProcessedSuggestion::new("foobar", "", "").with_type(SuggestionType::Folder),
985+
ProcessedSuggestion::new("foobar", "", "").with_type(SuggestionType::RegularFile),
986+
ProcessedSuggestion::new("foobar", "", "").with_type(SuggestionType::Misc),
987+
],
988+
unprocessed: std::collections::VecDeque::new(),
989+
common_prefix: None,
990+
auto_accept_if_solo: false,
991+
insert_common_prefix: false,
992+
comp_type: crate::tab_completion_context::CompType::FirstWord,
993+
nosort: false,
994+
compspec_was_useful: Some(true),
995+
should_run_flycomp: false,
996+
};
997+
998+
// Case 1: pattern "oo" - should only match RegularFile and Misc, but NOT Folder (as "oo" is not a prefix of "foobar").
999+
let active = ActiveSuggestions::new(
1000+
builder.clone(),
1001+
SubString::new("oo", "oo").unwrap(),
1002+
std::time::Duration::from_millis(0),
1003+
false,
1004+
crate::settings::SuggestionSortOrder::Alphabetical,
1005+
crate::settings::FuzzyMode::FolderPrefixes,
1006+
);
1007+
1008+
let filtered: Vec<usize> = active
1009+
.filtered_suggestions
1010+
.iter()
1011+
.map(|f| f.suggestion_idx)
1012+
.collect();
1013+
// Index 0 is Folder (no match), index 1 is RegularFile (match), index 2 is Misc (match)
1014+
assert_eq!(filtered, vec![1, 2]);
1015+
1016+
// Case 2: pattern "foo" - prefix matches Folder, and fuzzy matches all.
1017+
let active2 = ActiveSuggestions::new(
1018+
builder,
1019+
SubString::new("foo", "foo").unwrap(),
1020+
std::time::Duration::from_millis(0),
1021+
false,
1022+
crate::settings::SuggestionSortOrder::Alphabetical,
1023+
crate::settings::FuzzyMode::FolderPrefixes,
1024+
);
1025+
let filtered2: Vec<usize> = active2
1026+
.filtered_suggestions
1027+
.iter()
1028+
.map(|f| f.suggestion_idx)
1029+
.collect();
1030+
// Since "foo" is a prefix of "foobar", it matches Folder, RegularFile, and Misc.
1031+
assert_eq!(filtered2.len(), 3);
1032+
}
9611033
}
9621034

9631035
impl ProcessedSuggestion {
@@ -979,9 +1051,16 @@ impl ProcessedSuggestion {
9791051
suffix: suffix.into(),
9801052
style: None,
9811053
description: SuggestionDescription::Static(vec![]),
1054+
sug_type: SuggestionType::Misc,
9821055
}
9831056
}
9841057

1058+
/// Set the type on this suggestion.
1059+
pub fn with_type(mut self, suggestion_type: SuggestionType) -> Self {
1060+
self.sug_type = suggestion_type;
1061+
self
1062+
}
1063+
9851064
/// Set the description on this suggestion.
9861065
pub fn with_description(mut self, description: SuggestionDescription) -> Self {
9871066
self.description = description;
@@ -1191,9 +1270,18 @@ impl UnprocessedSuggestion {
11911270
SuggestionDescription::Static(vec![])
11921271
};
11931272

1273+
let suggestion_type = if path_to_use.as_ref().is_some_and(|p| p.is_dir()) {
1274+
SuggestionType::Folder
1275+
} else if comp_result_flags.filename_completion_desired || path_to_use.is_some() {
1276+
SuggestionType::RegularFile
1277+
} else {
1278+
SuggestionType::Misc
1279+
};
1280+
11941281
let suffix_str = suffix_char.map(|f| f.to_string()).unwrap_or_default();
11951282
let suggestion = ProcessedSuggestion::new(quoted_no_prefix, prefix, &suffix_str)
1196-
.with_description(description);
1283+
.with_description(description)
1284+
.with_type(suggestion_type);
11971285
match style {
11981286
Some(s) => suggestion.with_style(s),
11991287
None => suggestion,
@@ -1426,6 +1514,8 @@ pub struct ActiveSuggestions {
14261514
pub nosort: bool,
14271515
/// How to sort suggestions when fuzzy scores are tied.
14281516
pub sort_order: crate::settings::SuggestionSortOrder,
1517+
/// Controls fuzzy matching behavior for suggestions.
1518+
pub fuzzy_mode: crate::settings::FuzzyMode,
14291519
formatted_cache: Vec<Option<SuggestionFormatted>>,
14301520
max_width_cache: std::cell::Cell<Option<usize>>,
14311521
}
@@ -1437,6 +1527,7 @@ impl ActiveSuggestions {
14371527
load_time: std::time::Duration,
14381528
auto_started: bool,
14391529
sort_order: crate::settings::SuggestionSortOrder,
1530+
fuzzy_mode: crate::settings::FuzzyMode,
14401531
) -> Self {
14411532
let ActiveSuggestionsBuilder {
14421533
processed: processed_suggestions,
@@ -1473,6 +1564,7 @@ impl ActiveSuggestions {
14731564
auto_started,
14741565
nosort: nosort || sug_len > crate::FILENAME_INFERENCE_LIMIT,
14751566
sort_order,
1567+
fuzzy_mode,
14761568
formatted_cache: vec![],
14771569
max_width_cache: std::cell::Cell::new(Some(initial_max_width)),
14781570
};
@@ -1944,6 +2036,34 @@ impl ActiveSuggestions {
19442036
.strip_prefix(&sug.prefix)
19452037
.unwrap_or(pattern_with_prefix);
19462038

2039+
let fuzzy_enabled = match self.fuzzy_mode {
2040+
crate::settings::FuzzyMode::All => true,
2041+
crate::settings::FuzzyMode::None => false,
2042+
crate::settings::FuzzyMode::FolderPrefixes => match sug.sug_type {
2043+
crate::active_suggestions::SuggestionType::Folder => false,
2044+
_ => true,
2045+
},
2046+
};
2047+
2048+
if !fuzzy_enabled {
2049+
if pattern.is_empty() {
2050+
return Some(FilteredItem {
2051+
score: 0,
2052+
suggestion_idx: idx,
2053+
matching_indices: Vec::new(),
2054+
});
2055+
}
2056+
if sug.s.to_lowercase().starts_with(&pattern.to_lowercase()) {
2057+
let match_count = pattern.chars().count();
2058+
return Some(FilteredItem {
2059+
score: 1000,
2060+
suggestion_idx: idx,
2061+
matching_indices: (0..match_count).collect(),
2062+
});
2063+
}
2064+
return None;
2065+
}
2066+
19472067
// Try the fuzzy matcher first
19482068
if let Some((score, indices)) = crate::content_utils::fuzzy_indices_with_threshold(
19492069
&self.fuzzy_matcher,

src/app/tab_completion.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,7 @@ impl App<'_> {
11611161
load_time,
11621162
auto_started,
11631163
self.settings.suggestion_sort_order,
1164+
self.settings.fuzzy_mode,
11641165
);
11651166
self.content_mode = ContentMode::TabCompletion(Box::new(suggestions));
11661167
} else {
@@ -1176,6 +1177,7 @@ impl App<'_> {
11761177
load_time,
11771178
auto_started,
11781179
self.settings.suggestion_sort_order,
1180+
self.settings.fuzzy_mode,
11791181
);
11801182
self.content_mode = ContentMode::TabCompletion(Box::new(suggestions));
11811183
}
@@ -1497,6 +1499,7 @@ mod tab_completion_tests {
14971499
std::time::Duration::from_secs(0),
14981500
false,
14991501
crate::settings::SuggestionSortOrder::default(),
1502+
crate::settings::FuzzyMode::default(),
15001503
)
15011504
}
15021505

src/app/ui.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2353,6 +2353,7 @@ mod tests {
23532353
std::time::Duration::from_millis(0),
23542354
true, // auto_started
23552355
crate::settings::SuggestionSortOrder::default(),
2356+
crate::settings::FuzzyMode::default(),
23562357
);
23572358

23582359
// Select the first suggestion (sug1)
@@ -2434,6 +2435,7 @@ mod tests {
24342435
std::time::Duration::from_millis(0),
24352436
true, // auto_started
24362437
crate::settings::SuggestionSortOrder::default(),
2438+
crate::settings::FuzzyMode::default(),
24372439
);
24382440

24392441
// Select the first suggestion (sug1, which fits fully)
@@ -2502,6 +2504,7 @@ mod tests {
25022504
std::time::Duration::from_millis(0),
25032505
true, // auto_started
25042506
crate::settings::SuggestionSortOrder::default(),
2507+
crate::settings::FuzzyMode::default(),
25052508
);
25062509

25072510
// Select the first suggestion (sug1)
@@ -2589,6 +2592,7 @@ mod tests {
25892592
std::time::Duration::from_millis(0),
25902593
true, // auto_started
25912594
crate::settings::SuggestionSortOrder::default(),
2595+
crate::settings::FuzzyMode::default(),
25922596
);
25932597

25942598
// Render auto suggestions. Since y = 0, the suggestions box should take:

src/cli.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,10 @@ enum Commands {
506506
/// flyline suggestions --auto-suggest true --num-suggestion-rows 12
507507
#[command(name = "suggestions", verbatim_doc_comment)]
508508
Suggestions {
509+
/// Optional subcommand for suggestion actions.
510+
#[command(subcommand)]
511+
subcommand: Option<SuggestionsSubcommands>,
512+
509513
/// Enable or disable auto-suggest (auto-started tab completion suggestions).
510514
#[arg(long = "auto-suggest", default_missing_value = "true", num_args = 0..=1)]
511515
auto_suggest: Option<bool>,
@@ -559,6 +563,17 @@ enum Commands {
559563
Upgrade,
560564
}
561565

566+
#[derive(Subcommand, Debug)]
567+
enum SuggestionsSubcommands {
568+
/// Set fuzzy matching mode (all, none, no folders).
569+
#[command(name = "set-fuzzy-mode", verbatim_doc_comment)]
570+
SetFuzzyMode {
571+
/// The fuzzy mode to set (all, none, no folders).
572+
#[arg(value_name = "MODE")]
573+
mode: settings::FuzzyMode,
574+
},
575+
}
576+
562577
#[derive(Subcommand, Debug)]
563578
enum PerfSubcommands {
564579
/// Start recording performance metrics.
@@ -1235,13 +1250,22 @@ impl Flyline {
12351250
}
12361251
}
12371252
Some(Commands::Suggestions {
1253+
subcommand,
12381254
auto_suggest,
12391255
use_flycomp,
12401256
sort_order,
12411257
num_suggestion_rows,
12421258
flycomp_output,
12431259
flycomp_blacklist,
12441260
}) => {
1261+
if let Some(sub) = subcommand {
1262+
match sub {
1263+
SuggestionsSubcommands::SetFuzzyMode { mode } => {
1264+
log::info!("Fuzzy mode set to {:?}", mode);
1265+
self.settings.fuzzy_mode = mode;
1266+
}
1267+
}
1268+
}
12451269
if let Some(list) = flycomp_blacklist {
12461270
log::info!("Flycomp blacklist set to {:?}", list);
12471271
self.settings.flycomp_blacklist = list.into_iter().collect();

0 commit comments

Comments
 (0)