From c27d9f58fa21a646248c6db3ae78d92bb7ce8a5c Mon Sep 17 00:00:00 2001 From: Hal Frigaard <4559349+HalFrgrd@users.noreply.github.com> Date: Sat, 9 May 2026 08:52:49 +0100 Subject: [PATCH 01/10] dont insert common prefix for mid word file --- src/active_suggestions.rs | 8 ++++++++ src/app/tab_completion.rs | 29 +++++++++++++++++++---------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/active_suggestions.rs b/src/active_suggestions.rs index 2aba564..1f85b08 100644 --- a/src/active_suggestions.rs +++ b/src/active_suggestions.rs @@ -652,6 +652,7 @@ pub struct ActiveSuggestionsBuilder { pub processed: Vec, pub unprocessed: VecDeque, pub auto_accept_if_solo: bool, + pub insert_common_prefix: bool, pub common_prefix: Option, pub comp_type: tab_completion_context::CompType, } @@ -664,6 +665,7 @@ impl ActiveSuggestionsBuilder { processed: Vec::new(), unprocessed: VecDeque::new(), auto_accept_if_solo: true, + insert_common_prefix: true, common_prefix: None, comp_type: tab_completion_context::CompType::default(), } @@ -677,6 +679,11 @@ impl ActiveSuggestionsBuilder { self } + pub fn with_insert_common_prefix(mut self, insert_common_prefix: bool) -> Self { + self.insert_common_prefix = insert_common_prefix; + self + } + pub fn with_comp_type(mut self, comp_type: tab_completion_context::CompType) -> Self { self.comp_type = comp_type; self @@ -832,6 +839,7 @@ impl ActiveSuggestions { unprocessed: unprocessed_suggestions, common_prefix: _, auto_accept_if_solo: _, + insert_common_prefix: _, comp_type, } = builder; let sug_len = processed_suggestions.len() + unprocessed_suggestions.len(); diff --git a/src/app/tab_completion.rs b/src/app/tab_completion.rs index 50b55cc..50c5d0f 100644 --- a/src/app/tab_completion.rs +++ b/src/app/tab_completion.rs @@ -194,17 +194,20 @@ pub(crate) fn gen_completions_internal( ) -> Option { let mut builder = gen_completions_uncomitted(completion_context)?; - let all_processed = builder.try_process_all(); + let all_processed = if cfg!(test) { + // Tests demand determinism: process everything and always compute + // the common prefix even if `insert_common_prefix` is false. + while !builder.try_process_all() {} + true + } else { + builder.try_process_all() + }; + if !all_processed { log::debug!("Not all suggestions were fully processed; skipping common prefix calculation"); } - if cfg!(test) { - // Tests demand determinism: process everything and always compute - // the common prefix even if `auto_accept_if_solo` is false. - while !builder.try_process_all() {} - builder.set_common_prefix(); - } else if builder.auto_accept_if_solo && all_processed { + if builder.insert_common_prefix && all_processed { builder.set_common_prefix(); } @@ -336,7 +339,9 @@ fn gen_completions_uncomitted( .map(|_score| sug) }) .collect(); - builder = builder.with_auto_accept_if_solo(false); + builder = builder + .with_auto_accept_if_solo(false) + .with_insert_common_prefix(false); log::debug!( "CompType::FuzzyCommandComp found {} completions for pattern: {}", builder.len(), @@ -465,6 +470,9 @@ fn gen_completions_uncomitted( if !completions.is_empty() { return Some( ActiveSuggestionsBuilder::from_unprocessed(completions) + .with_insert_common_prefix( + completion_context.word_right_of_cursor().is_empty(), + ) .with_comp_type(comp_type.clone()), ); } @@ -486,6 +494,7 @@ fn gen_completions_uncomitted( return Some( ActiveSuggestionsBuilder::from_unprocessed(completions) .with_auto_accept_if_solo(false) + .with_insert_common_prefix(false) .with_comp_type(comp_type.clone()), ); } @@ -1290,8 +1299,8 @@ mod tab_completion_tests { let outcome = apply_tab_complete_to_buffer(&mut buffer, &builder, &comp_context.word_under_cursor); log::info!("Outcome of applying tab complete: {:?}", &outcome); - assert!(matches!(outcome, TabCompleteBufferOutcome::Pending { ref final_wuc } if final_wuc.as_ref() == "./foo")); - assert_eq!(buffer.buffer(), "mycmd ./foo"); + assert!(matches!(outcome, TabCompleteBufferOutcome::Pending { ref final_wuc } if final_wuc.as_ref() == "./fo/barA")); + assert_eq!(buffer.buffer(), "mycmd ./fo/barA"); } // #[test] From 45e0a3475982054ad2827da637e3af3e9842f46e Mon Sep 17 00:00:00 2001 From: Hal Frigaard <4559349+HalFrgrd@users.noreply.github.com> Date: Sat, 9 May 2026 08:55:41 +0100 Subject: [PATCH 02/10] more test files --- tests/example_braces_fs/foo2/asdA | 0 tests/example_braces_fs/foo3/asdA | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/example_braces_fs/foo2/asdA create mode 100644 tests/example_braces_fs/foo3/asdA diff --git a/tests/example_braces_fs/foo2/asdA b/tests/example_braces_fs/foo2/asdA new file mode 100644 index 0000000..e69de29 diff --git a/tests/example_braces_fs/foo3/asdA b/tests/example_braces_fs/foo3/asdA new file mode 100644 index 0000000..e69de29 From 4492a7828cd74b25f6b8cb2324912ebf5a936757 Mon Sep 17 00:00:00 2001 From: Hal Frigaard <4559349+HalFrgrd@users.noreply.github.com> Date: Sat, 9 May 2026 09:08:36 +0100 Subject: [PATCH 03/10] start of fuzzy globbing --- src/app/tab_completion.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/tab_completion.rs b/src/app/tab_completion.rs index 50c5d0f..83f37df 100644 --- a/src/app/tab_completion.rs +++ b/src/app/tab_completion.rs @@ -729,7 +729,7 @@ fn tab_complete_fuzzy_filename( let matcher = ArinaeMatcher::new(skim::CaseMatching::Smart, true); - // glob expansion handles dequoting the pattern, so we only need to dequote + // glob expansion handles dequoting the pattern, so we only need to dequote the remaining fragment let dequoted_fragment = bash_funcs::dequoting_function_rust(&filename_fragment); let mut scored: Vec<(i64, UnprocessedSuggestion)> = all_files From b2ad904d60b0f85971b466e2f658b516a7270221 Mon Sep 17 00:00:00 2001 From: Hal Frigaard <4559349+HalFrgrd@users.noreply.github.com> Date: Sat, 9 May 2026 09:27:29 +0100 Subject: [PATCH 04/10] passing --- src/app/tab_completion.rs | 301 ++++++++++++++++++++++++---------- src/bash_funcs.rs | 8 +- src/tab_completion_context.rs | 7 +- src/text_buffer.rs | 13 ++ 4 files changed, 234 insertions(+), 95 deletions(-) diff --git a/src/app/tab_completion.rs b/src/app/tab_completion.rs index 83f37df..0c73394 100644 --- a/src/app/tab_completion.rs +++ b/src/app/tab_completion.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::path::{Path, PathBuf}; use std::vec; use crate::active_suggestions::{ @@ -483,7 +484,7 @@ fn gen_completions_uncomitted( word_under_cursor.as_ref() ); let (completions, _comp_res_flags) = - tab_complete_fuzzy_filename(word_under_cursor.as_ref()); + tab_complete_fuzzy_filename(completion_context); log::debug!( "CompType::FuzzyFilenameExpansion found {} completions for pattern: {}", @@ -569,7 +570,7 @@ fn tab_complete_fuzzy_first_word(command: &str) -> ActiveSuggestionsBuilder { } if command.starts_with('.') || command.contains('/') || command.starts_with('~') { - let (fuzzy_files, _comp_res_flags) = tab_complete_fuzzy_filename(command); + let (fuzzy_files, _comp_res_flags) = tab_complete_fuzzy_filename_from_word(command); let executable_files = filter_out_non_executables(fuzzy_files); return ActiveSuggestionsBuilder::from_unprocessed(executable_files); } @@ -697,66 +698,171 @@ fn tab_complete_glob_expansion( /// This is the fallback when [`tab_complete_glob_expansion`] (prefix matching) /// finds no results: e.g. typing `src/tm` won't prefix-match `src/tab_completion.rs`, /// but the fuzzy matcher will. +fn tab_complete_fuzzy_filename_from_word( + word_under_cursor: &str, +) -> (Vec, bash_funcs::CompletionFlags) { + tab_complete_fuzzy_filename_impl(word_under_cursor, 0) +} + fn tab_complete_fuzzy_filename( + completion_context: &tab_completion_context::CompletionContext, +) -> (Vec, bash_funcs::CompletionFlags) { + let cursor_seg_from_right = completion_context + .word_right_of_cursor() + .matches('/') + .count(); + tab_complete_fuzzy_filename_impl( + completion_context.word_under_cursor.as_ref(), + cursor_seg_from_right, + ) +} + +fn tab_complete_fuzzy_filename_impl( word_under_cursor: &str, + cursor_seg_from_right: usize, ) -> (Vec, bash_funcs::CompletionFlags) { let mut comp_res_flags = bash_funcs::CompletionFlags::default(); - // Split at the last '/' to separate the directory prefix from the filename - // fragment that will be used as the fuzzy-match pattern. - - let (dir_glob_pattern, filename_fragment) = - if let Some(slash_pos) = word_under_cursor.rfind('/') { - ( - word_under_cursor[..slash_pos + 1].to_string() + "*", - word_under_cursor[slash_pos + 1..].to_string(), - ) - } else { - ("*".to_string(), word_under_cursor.to_string()) - }; + comp_res_flags.filename_quoting_desired = false; + comp_res_flags.filename_completion_desired = true; + comp_res_flags.quote_type = bash_funcs::find_quote_type(word_under_cursor); - // Nothing to fuzzy-match against — let the caller fall through. - if filename_fragment.is_empty() { + let dequoted_wuc = bash_funcs::dequoting_function_rust(word_under_cursor); + let (is_absolute, segments) = split_nonempty_path_segments(&dequoted_wuc); + if segments.is_empty() { return (vec![], comp_res_flags); } - // Set up flags for glob expansion - comp_res_flags.filename_quoting_desired = false; - comp_res_flags.filename_completion_desired = true; - comp_res_flags.quote_type = bash_funcs::find_quote_type(&dir_glob_pattern); + let cursor_seg_idx = segments + .len() + .saturating_sub(cursor_seg_from_right.saturating_add(1)); + let (prefix_segments, fuzzy_segments) = segments.split_at(cursor_seg_idx); + if fuzzy_segments.is_empty() { + return (vec![], comp_res_flags); + } - let expanded = PathPatternExpansion::new(&dir_glob_pattern); - let all_files = tab_complete_with_expanded_pattern(&expanded, comp_res_flags, false); + let base_input = path_from_segments(is_absolute, prefix_segments); + let expanded_base = PathBuf::from(bash_funcs::fully_expand_path(if base_input.is_empty() { + "." + } else { + &base_input + })); + let raw_prefix = path_prefix_for_output(is_absolute, prefix_segments); let matcher = ArinaeMatcher::new(skim::CaseMatching::Smart, true); + let mut scored = fuzzy_glob_recursive(&expanded_base, fuzzy_segments, &matcher); + if scored.is_empty() { + return (vec![], comp_res_flags); + } - // glob expansion handles dequoting the pattern, so we only need to dequote the remaining fragment - let dequoted_fragment = bash_funcs::dequoting_function_rust(&filename_fragment); + scored.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(&b.1))); + scored.dedup_by(|a, b| a.1 == b.1); - let mut scored: Vec<(i64, UnprocessedSuggestion)> = all_files + let completions = scored .into_iter() - .filter_map(|sug| { - // Match only against the last path segment so that e.g. the - // directory prefix doesn't inflate the score. - let match_text = sug.match_text(); - let filename = match_text.rsplit('/').next().unwrap_or(match_text); - content_utils::fuzzy_match_with_threshold( - &matcher, - filename, - &dequoted_fragment, - content_utils::FuzzyMatchThreshold::Medium, - ) - .map(|score| (score, sug)) + .map(|(_score, matched_segments, final_path)| { + let mut raw_text = raw_prefix.clone(); + raw_text.push_str(&matched_segments.join("/")); + + UnprocessedSuggestion { + raw_text, + full_path: Some(final_path), + flags: comp_res_flags, + word_under_cursor: String::new(), + } }) .collect(); - // Best matches first. - scored.sort_by(|a, b| b.0.cmp(&a.0)); - scored.dedup_by(|a, b| a.1.match_text() == b.1.match_text()); - let completions = scored.into_iter().map(|(_, sug)| sug).collect(); - (completions, comp_res_flags) } +fn split_nonempty_path_segments(path: &str) -> (bool, Vec) { + let is_absolute = path.starts_with('/'); + let segments = path + .split('/') + .filter(|seg| !seg.is_empty()) + .map(ToString::to_string) + .collect(); + (is_absolute, segments) +} + +fn path_from_segments(is_absolute: bool, segments: &[String]) -> String { + if segments.is_empty() { + if is_absolute { + "/".to_string() + } else { + String::new() + } + } else { + let mut out = String::new(); + if is_absolute { + out.push('/'); + } + out.push_str(&segments.join("/")); + out + } +} + +fn path_prefix_for_output(is_absolute: bool, segments: &[String]) -> String { + let mut out = path_from_segments(is_absolute, segments); + if !out.is_empty() && !out.ends_with('/') { + out.push('/'); + } + out +} + +fn fuzzy_glob_recursive( + base_dir: &Path, + remaining_segments: &[String], + matcher: &ArinaeMatcher, +) -> Vec<(i64, Vec, PathBuf)> { + if remaining_segments.is_empty() { + return vec![(0, vec![], base_dir.to_path_buf())]; + } + + let mut out = Vec::new(); + let pattern = &remaining_segments[0]; + let is_last = remaining_segments.len() == 1; + + let Ok(entries) = std::fs::read_dir(base_dir) else { + return out; + }; + + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + let Some(score) = content_utils::fuzzy_match_with_threshold( + matcher, + &name, + pattern, + content_utils::FuzzyMatchThreshold::Medium, + ) else { + continue; + }; + + let path = entry.path(); + let file_type = entry.file_type().ok(); + + if is_last { + out.push((score, vec![name], path)); + continue; + } + + if !file_type.is_some_and(|ft| ft.is_dir()) { + continue; + } + + for (child_score, child_segments, final_path) in + fuzzy_glob_recursive(&path, &remaining_segments[1..], matcher) + { + let mut segments = Vec::with_capacity(1 + child_segments.len()); + segments.push(name.clone()); + segments.extend(child_segments); + out.push((score + child_score, segments, final_path)); + } + } + + out +} + fn tab_complete_tilde_expansion(pattern: &str) -> Vec { let user_pattern = if let Some(stripped) = pattern.strip_prefix('~') { stripped @@ -1061,6 +1167,11 @@ mod tab_completion_tests { std::env::set_current_dir(&dir).unwrap_or_else(|e| panic!("cd {dir}: {e}")); } + fn cd_to_example_fuzzy_glob_fs() { + let dir = find_test_fixture_dir("example_fuzzy_glob_fs"); + std::env::set_current_dir(&dir).unwrap_or_else(|e| panic!("cd {dir}: {e}")); + } + rusty_fork_test! { // ------- dummy git completion (clap-based, no bash symbols) ------- @@ -1107,15 +1218,12 @@ mod tab_completion_tests { #[test] fn git_diff_dashdash_lists_long_flags_mid_word() { cd_to_example_fs(); - let mut buffer = TextBuffer::new("git diff --stag"); + let buffer = TextBuffer::new_with_cursor("git diff --st█ag"); // It doesnt matter where the cursor is because I always move it to the end // This gives best results since it allows the FuzzyCommandComp and Filname (that uses mid word information) // to run. - buffer.move_to_end(); // --stag| - buffer.move_left(); // --sta|g - buffer.move_left(); // --st|ag let actual = run_completion_from_buffer(&buffer); let names: Vec<&str> = actual.iter().map(|s| s.s.as_str()).collect(); for flag in ["--staged"] { @@ -1124,7 +1232,7 @@ mod tab_completion_tests { // If we didnt move the cursor to the end, // we would get the same results as this one: - let buffer = TextBuffer::new("git diff --st"); + let buffer = TextBuffer::new_with_cursor("git diff --st█"); let actual = run_completion_from_buffer(&buffer); let names: Vec<&str> = actual.iter().map(|s| s.s.as_str()).collect(); for flag in ["--staged", "--stat"] { @@ -1240,16 +1348,25 @@ mod tab_completion_tests { ); } + #[test] + fn fuzzy_globbing_recurses_across_path_segments() { + cd_to_example_fuzzy_glob_fs(); + + let buffer = TextBuffer::new_with_cursor("mycmd ./tr█e/lefa/apel"); + + let builder = get_builder_from_buffer(&buffer).unwrap().0; + assert_eq!(builder.comp_type, CompType::FuzzyFilenameExpansion); + + let names: Vec<&str> = builder.processed.iter().map(|s| s.s.as_str()).collect(); + assert!(names.contains(&"./tree/leaf/apple.txt")); + assert!(names.contains(&"./three/leaf/apple.log")); + } + #[test] fn mid_word_completion() { cd_to_example_fs(); - let mut buffer = TextBuffer::new("mycmd ./abc/f/baz"); - buffer.move_left(); - buffer.move_left(); - buffer.move_left(); - buffer.move_left(); // cursor is now right after f - + let mut buffer = TextBuffer::new_with_cursor("mycmd ./abc/f█/baz"); let (builder, comp_context) = get_builder_from_buffer(&buffer).unwrap(); assert_eq!(builder.comp_type, CompType::FilenameExpansion); @@ -1270,13 +1387,7 @@ mod tab_completion_tests { #[test] fn mid_word_completion_multiple() { cd_to_example_braces_fs(); - let mut buffer = TextBuffer::new("mycmd ./fo/barA"); - buffer.move_left(); - buffer.move_left(); - buffer.move_left(); - buffer.move_left(); - buffer.move_left(); // cursor is now right after f - + let mut buffer = TextBuffer::new_with_cursor("mycmd ./fo█/barA"); let (builder, comp_context) = get_builder_from_buffer(&buffer).unwrap(); assert_eq!(builder.comp_type, CompType::FilenameExpansion); @@ -1303,33 +1414,49 @@ mod tab_completion_tests { assert_eq!(buffer.buffer(), "mycmd ./fo/barA"); } - // #[test] - // fn mid_word_completion_naive_bash_default() { - // cd_to_example_fs(); - // // Cat is setup so that run_programmable_completions in test fixtures - // // returns files matching the lhs of - // let mut buffer = TextBuffer::new("cat ./abc/f/baz"); - // buffer.move_left(); - // buffer.move_left(); - // buffer.move_left(); - // buffer.move_left(); // cursor is now right after f - - - // let (builder, comp_context) = get_builder_from_buffer(&buffer).unwrap(); - // assert_eq!(builder.comp_type, CompType::FilenameExpansion); - // assert_processed( - // &builder.processed, - // &[ProcessedSuggestion::new( - // "./abc/foo/baz", - // "", - // " ", - // )], - // ); - - // let outcome = apply_tab_complete_to_buffer(&mut buffer, &builder, &comp_context.word_under_cursor); - // assert!(matches!(outcome, TabCompleteBufferOutcome::SoloAccepted)); - // assert_eq!(buffer.buffer(), "casdfat ./abc/foo/baz "); - // } + #[test] + fn mid_word_completion_naive_bash_default() { + cd_to_example_fs(); + // Cat is setup so that run_programmable_completions in test fixtures + // returns files matching the lhs of + + // We move the cursor to the end so this acts like "./abc/foo/ba█" + // Which a naive glob will complete + let mut buffer = TextBuffer::new_with_cursor("cat ./abc/foo█/ba"); + + let (builder, comp_context) = get_builder_from_buffer(&buffer).unwrap(); + assert_eq!(builder.comp_type, CompType::CommandComp { command_word: "cat".to_string() }); + assert_processed( + &builder.processed, + &[ProcessedSuggestion::new( + "./abc/foo/baz", + "", + " ", + )], + ); + let outcome = apply_tab_complete_to_buffer(&mut buffer, &builder, &comp_context.word_under_cursor); + assert!(matches!(outcome, TabCompleteBufferOutcome::SoloAccepted)); + assert_eq!(buffer.buffer(), "cat ./abc/foo/baz "); + + + // But now since fo folder doesnt exit (only 'foo' does) + // command comp should fail we fall back to fuzzy filename + let mut buffer = TextBuffer::new_with_cursor("cat ./abc/fo█/ba"); + + let (builder, comp_context) = get_builder_from_buffer(&buffer).unwrap(); + assert_eq!(builder.comp_type, CompType::FuzzyFilenameExpansion); + assert_processed( + &builder.processed, + &[ProcessedSuggestion::new( + "./abc/foo/baz", + "", + " ", + )], + ); + let outcome = apply_tab_complete_to_buffer(&mut buffer, &builder, &comp_context.word_under_cursor); + assert!(matches!(outcome, TabCompleteBufferOutcome::Pending { ref final_wuc } if final_wuc.as_ref() == "./abc/fo/ba")); + assert_eq!(buffer.buffer(), "cat ./abc/fo/ba"); + } diff --git a/src/bash_funcs.rs b/src/bash_funcs.rs index 2f82d71..cc5ba7e 100644 --- a/src/bash_funcs.rs +++ b/src/bash_funcs.rs @@ -912,12 +912,8 @@ pub fn expand_filename(filename: &str) -> String { expanded = expanded.replace(&braced, &value).replace(&unbraced, &value); } - if !Path::new(&expanded).exists() { - panic!( - "[test] expand_filename: expanded path does not exist: input={:?} expanded={:?}", - filename, expanded - ); - } + assert!(!expanded.contains("$")); + assert!(!expanded.contains("~")); expanded } diff --git a/src/tab_completion_context.rs b/src/tab_completion_context.rs index 6d95b02..5860d3f 100644 --- a/src/tab_completion_context.rs +++ b/src/tab_completion_context.rs @@ -448,6 +448,8 @@ pub fn get_completion_context<'a>( #[cfg(test)] mod tests { + use crate::text_buffer::TextBuffer; + use super::*; fn run<'a>(input: &'a str, cursor_byte_pos: usize) -> CompletionContext<'a> { @@ -457,8 +459,9 @@ mod tests { /// Parse a test string with `█` marking the cursor position. /// Returns (input_without_cursor, cursor_byte_pos). fn run_inline(input: &str) -> CompletionContext<'static> { - let cursor_byte_pos = input.find('█').expect("Cursor marker █ not found"); - let input_without_cursor = input.replace('█', ""); + let buffer = TextBuffer::new_with_cursor(input); + let cursor_byte_pos = buffer.cursor_byte_pos(); + let input_without_cursor = buffer.buffer().to_string(); let input_without_cursor: &'static str = Box::leak(input_without_cursor.into_boxed_str()); run(input_without_cursor, cursor_byte_pos) } diff --git a/src/text_buffer.rs b/src/text_buffer.rs index 0f4ed83..fd182de 100644 --- a/src/text_buffer.rs +++ b/src/text_buffer.rs @@ -80,6 +80,19 @@ impl TextBuffer { undo_redo: SnapshotManager::new(), } } + + #[cfg(test)] + pub fn new_with_cursor(starting_str: &str) -> Self { + let cursor_byte_pos = starting_str.find('█').expect("Cursor marker █ not found"); + let input_without_cursor = starting_str.replace('█', ""); + + TextBuffer { + buf: input_without_cursor, + cursor_byte: cursor_byte_pos, + selection_byte: None, + undo_redo: SnapshotManager::new(), + } + } } ///////////////////////////////////////////////////////// text selection From cc3d49092ab6c2f09ed8e870d15baa2f9d84f8a2 Mon Sep 17 00:00:00 2001 From: Hal Frigaard <4559349+HalFrgrd@users.noreply.github.com> Date: Sat, 9 May 2026 09:32:03 +0100 Subject: [PATCH 05/10] formatting time --- src/app/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index c64ce24..346cac9 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2141,12 +2141,12 @@ impl<'a> App<'a> { content.write_tagged_span(&TaggedSpan::new( Span::styled( format!( - "# Pos: {}; Filtered: {}/{}; {} ({}ms)", + "# Pos: {}; Filtered: {}/{}; {} ({:.1}ms)", pos_string, active_suggestions.filtered_suggestions_len(), active_suggestions.all_suggestions_len(), active_suggestions.comp_type.display_name(), - active_suggestions.load_time.as_millis(), + active_suggestions.load_time.as_secs_f32() * 1000.0, ), self.settings.colour_palette.secondary_text(), ), From e4a1e13812325fc932949dd57b788fabeb8c4ece Mon Sep 17 00:00:00 2001 From: Hal Frigaard <4559349+HalFrgrd@users.noreply.github.com> Date: Sat, 9 May 2026 09:36:14 +0100 Subject: [PATCH 06/10] fuzzy glob and long filename examples --- tests/example_fuzzy_glob_fs/three/leaf/apple.log | 0 tests/example_fuzzy_glob_fs/three/left/apex.txt | 0 tests/example_fuzzy_glob_fs/tree/leaf/apple.txt | 0 tests/example_fuzzy_glob_fs/trunk/leaf/banana.txt | 0 tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 + .../example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 + ...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 + ...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 + ...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 + 9 files changed, 5 insertions(+) create mode 100644 tests/example_fuzzy_glob_fs/three/leaf/apple.log create mode 100644 tests/example_fuzzy_glob_fs/three/left/apex.txt create mode 100644 tests/example_fuzzy_glob_fs/tree/leaf/apple.txt create mode 100644 tests/example_fuzzy_glob_fs/trunk/leaf/banana.txt create mode 100644 tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa create mode 100644 tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa create mode 100644 tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa create mode 100644 tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa create mode 100644 tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa diff --git a/tests/example_fuzzy_glob_fs/three/leaf/apple.log b/tests/example_fuzzy_glob_fs/three/leaf/apple.log new file mode 100644 index 0000000..e69de29 diff --git a/tests/example_fuzzy_glob_fs/three/left/apex.txt b/tests/example_fuzzy_glob_fs/three/left/apex.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/example_fuzzy_glob_fs/tree/leaf/apple.txt b/tests/example_fuzzy_glob_fs/tree/leaf/apple.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/example_fuzzy_glob_fs/trunk/leaf/banana.txt b/tests/example_fuzzy_glob_fs/trunk/leaf/banana.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa b/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa @@ -0,0 +1 @@ +test diff --git a/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa b/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa @@ -0,0 +1 @@ +test diff --git a/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa b/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa @@ -0,0 +1 @@ +test diff --git a/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa b/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa @@ -0,0 +1 @@ +test diff --git a/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa b/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa @@ -0,0 +1 @@ +test From 4c0181537c8b034390ec2723d29209519cf6f670 Mon Sep 17 00:00:00 2001 From: Hal Frigaard <4559349+HalFrgrd@users.noreply.github.com> Date: Sat, 9 May 2026 10:23:28 +0100 Subject: [PATCH 07/10] Handle pattern too long --- src/active_suggestions.rs | 23 ++- src/app/tab_completion.rs | 144 ++++++++++++++++++ ...aaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbb | 0 ...bcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_BAR | 0 ...bcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_FOO | 0 5 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbb create mode 100644 tests/example_long_filenames_fs/len_65_plus_3/abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_BAR create mode 100644 tests/example_long_filenames_fs/len_65_plus_3/abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_FOO diff --git a/src/active_suggestions.rs b/src/active_suggestions.rs index 1f85b08..a7fb422 100644 --- a/src/active_suggestions.rs +++ b/src/active_suggestions.rs @@ -1181,13 +1181,26 @@ impl ActiveSuggestions { .strip_prefix(&sug.prefix) .unwrap_or(pattern_with_prefix); - self.fuzzy_matcher - .fuzzy_indices(&sug.s, pattern) - .map(|(score, indices)| FilteredItem { + // Try the fuzzy matcher first + if let Some((score, indices)) = self.fuzzy_matcher.fuzzy_indices(&sug.s, pattern) { + return Some(FilteredItem { score, suggestion_idx: idx, matching_indices: indices, - }) + }); + } + + const MAX_PATTERN_LENGTH: usize = 64; + if pattern.len() > MAX_PATTERN_LENGTH { + return Some(FilteredItem { + score: 0, + suggestion_idx: idx, + matching_indices: Vec::new(), + }); + } + + // No match: filter this out + None } pub fn update_word_under_cursor(&mut self, new_word_under_cursor: &SubString) { @@ -1205,7 +1218,7 @@ impl ActiveSuggestions { self.unprocessed_suggestions.len() ); - // Score and filter processed suggestions using the stored matcher. + self.filtered_suggestions = self .processed_suggestions .iter() diff --git a/src/app/tab_completion.rs b/src/app/tab_completion.rs index 0c73394..88f7e76 100644 --- a/src/app/tab_completion.rs +++ b/src/app/tab_completion.rs @@ -1172,6 +1172,11 @@ mod tab_completion_tests { std::env::set_current_dir(&dir).unwrap_or_else(|e| panic!("cd {dir}: {e}")); } + fn cd_to_example_long_filenames_fs() { + let dir = find_test_fixture_dir("example_long_filenames_fs"); + std::env::set_current_dir(&dir).unwrap_or_else(|e| panic!("cd {dir}: {e}")); + } + rusty_fork_test! { // ------- dummy git completion (clap-based, no bash symbols) ------- @@ -1489,5 +1494,144 @@ mod tab_completion_tests { assert!(matches!(outcome, TabCompleteBufferOutcome::Pending { .. })); assert_eq!(buffer.buffer(), "mycmd foo"); } + + // ------- fuzzy matching with long filenames ----------- + + #[test] + fn fuzzy_matching_with_long_filenames() { + cd_to_example_long_filenames_fs(); + let load_time = std::time::Duration::from_millis(0); + + // Read the directory and verify we have the expected filenames + let entries: Vec = std::fs::read_dir(".") + .unwrap() + .filter_map(|e| e.ok()) + .filter_map(|e| { + let path = e.path(); + if path.is_file() { + path.file_name().and_then(|n| n.to_str()).map(|s| s.to_string()) + } else { + None + } + }) + .collect(); + + // Verify we have files of the expected lengths + assert!(entries.iter().any(|s| s.len() == 32), "Missing 32-char filename"); + assert!(entries.iter().any(|s| s.len() == 33), "Missing 33-char filename"); + assert!(entries.iter().any(|s| s.len() == 64), "Missing 64-char filename"); + assert!(entries.iter().any(|s| s.len() == 65), "Missing 65-char filename"); + assert!(entries.iter().any(|s| s.len() == 100), "Missing 100-char filename"); + + // Create ProcessedSuggestions for each filename + let suggestions: Vec = entries + .iter() + .map(|name| ProcessedSuggestion::new(name.clone(), "", " ")) + .collect(); + +// Test 1: Normal pattern (within 32 char limit) - fuzzy matching works + let pattern_normal = "aaa"; + let word_under_cursor_normal = SubString::from_parts(pattern_normal.to_string(), 0); + let builder = ActiveSuggestionsBuilder::from_processed(suggestions.clone()); + let active_sugg = ActiveSuggestions::new(builder, word_under_cursor_normal, load_time); + let filtered_count_normal = active_sugg.filtered_suggestions_len(); + + log::info!( + "Test 1 (normal pattern, 3 chars) - Pattern: 'aaa', Filtered suggestions: {}", + filtered_count_normal + ); + // Should match all files since they all start with 'aaa...' + assert!( + filtered_count_normal > 0, + "Expected fuzzy matches for 'aaa' pattern" + ); + +// Test 2: Pattern exactly at limit (32 chars) - fuzzy matching works + let pattern_at_limit = "a".repeat(32); + let word_under_cursor_32 = SubString::from_parts(pattern_at_limit.clone(), 0); + let builder = ActiveSuggestionsBuilder::from_processed(suggestions.clone()); + let active_sugg = ActiveSuggestions::new(builder, word_under_cursor_32, load_time); + let filtered_count_32 = active_sugg.filtered_suggestions_len(); + + log::info!( + "Test 2 (at limit, 32 chars) - Pattern: 'a'x32, Filtered suggestions: {}", + filtered_count_32 + ); + // At the limit, fuzzy matcher should still work + assert!( + filtered_count_32 > 0, + "Expected fuzzy matches for 32-char pattern (at limit)" + ); + +// Test 3: Pattern slightly over limit (33 chars) - returns dummy items, filtered to 0 + let pattern_over_limit = "a".repeat(33); + let word_under_cursor_33 = SubString::from_parts(pattern_over_limit.clone(), 0); + let builder = ActiveSuggestionsBuilder::from_processed(suggestions.clone()); + let active_sugg = ActiveSuggestions::new(builder, word_under_cursor_33, load_time); + let filtered_count_33 = active_sugg.filtered_suggestions_len(); + + log::info!( + "Test 3 (over limit, 33 chars) - Pattern: 'a'x33, Filtered suggestions: {}", + filtered_count_33 + ); + // Over the limit, fuzzy matcher returns None, we return score-0 dummy items, + // which are filtered out, so we should get 0 results + assert_eq!( + filtered_count_33, 0, + "Expected no matches for 33-char pattern (fuzzy matcher limit exceeded)" + ); + + // Test 4: Pattern at 64 chars (well over limit) + let pattern_64 = "a".repeat(64); + let word_under_cursor_64 = SubString::from_parts(pattern_64.clone(), 0); + let builder = ActiveSuggestionsBuilder::from_processed(suggestions.clone()); + let active_sugg = ActiveSuggestions::new(builder, word_under_cursor_64, load_time); + let filtered_count_64 = active_sugg.filtered_suggestions_len(); + + log::info!( + "Test 4 (64 chars) - Pattern: 'a'x64, Filtered suggestions: {}", + filtered_count_64 + ); + // Well over limit, should result in 0 matches + assert_eq!( + filtered_count_64, 0, + "Expected no matches for 64-char pattern (well over limit)" + ); + + // Test 5: Pattern at 100 chars + let pattern_100 = "a".repeat(100); + let word_under_cursor_100 = SubString::from_parts(pattern_100.clone(), 0); + let builder = ActiveSuggestionsBuilder::from_processed(suggestions.clone()); + let active_sugg = ActiveSuggestions::new(builder, word_under_cursor_100, load_time); + let filtered_count_100 = active_sugg.filtered_suggestions_len(); + + log::info!( + "Test 5 (100 chars) - Pattern: 'a'x100, Filtered suggestions: {}", + filtered_count_100 + ); + // Well over limit, should result in 0 matches + assert_eq!( + filtered_count_100, 0, + "Expected no matches for 100-char pattern (well over limit)" + ); + + // Test 6: Pattern longer than all files (150 chars) + let pattern_150 = "a".repeat(150); + let word_under_cursor_150 = SubString::from_parts(pattern_150.clone(), 0); + let builder = ActiveSuggestionsBuilder::from_processed(suggestions.clone()); + let active_sugg = ActiveSuggestions::new(builder, word_under_cursor_150, load_time); + let filtered_count_150 = active_sugg.filtered_suggestions_len(); + + log::info!( + "Test 6 (150 chars) - Pattern: 'a'x150, Filtered suggestions: {}", + filtered_count_150 + ); + // Pattern much longer than all files should result in 0 matches + assert_eq!( + filtered_count_150, 0, + "Expected no matches for pattern (len {}) longer than all files", + pattern_150.len() + ); + } } } diff --git a/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbb b/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbb new file mode 100644 index 0000000..e69de29 diff --git a/tests/example_long_filenames_fs/len_65_plus_3/abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_BAR b/tests/example_long_filenames_fs/len_65_plus_3/abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_BAR new file mode 100644 index 0000000..e69de29 diff --git a/tests/example_long_filenames_fs/len_65_plus_3/abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_FOO b/tests/example_long_filenames_fs/len_65_plus_3/abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_FOO new file mode 100644 index 0000000..e69de29 From 8e5e8291d7a091ac99f64a832c6e1b0d5297e125 Mon Sep 17 00:00:00 2001 From: Hal Frigaard <4559349+HalFrgrd@users.noreply.github.com> Date: Sat, 9 May 2026 10:56:22 +0100 Subject: [PATCH 08/10] Test case for long filename --- src/active_suggestions.rs | 36 ++- src/app/tab_completion.rs | 229 +++++++----------- .../aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 - .../aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 - ...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 - ...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 - ...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | 1 - ...d_abcd_abcd_abcd_abcd_abcd_abcd_abcd_aBAR} | 0 ...cd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_aFOO | 0 9 files changed, 116 insertions(+), 154 deletions(-) delete mode 100644 tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa delete mode 100644 tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa delete mode 100644 tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa delete mode 100644 tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa delete mode 100644 tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa rename tests/example_long_filenames_fs/{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbb => len_61_plus_3/abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_aBAR} (100%) create mode 100644 tests/example_long_filenames_fs/len_61_plus_3/abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_aFOO diff --git a/src/active_suggestions.rs b/src/active_suggestions.rs index a7fb422..c980eef 100644 --- a/src/active_suggestions.rs +++ b/src/active_suggestions.rs @@ -779,11 +779,11 @@ impl ActiveSuggestionsBuilder { /// expensive rendering work is done on demand in [`ActiveSuggestions::into_grid`]. /// /// `suggestion_idx` is an index into [`ActiveSuggestions::processed_suggestions`]. -#[derive(Debug, Clone)] -struct FilteredItem { - suggestion_idx: usize, - score: i64, - matching_indices: Vec, +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FilteredItem { + pub suggestion_idx: usize, + pub score: i64, + pub matching_indices: Vec, } pub struct ColumnInfo { @@ -800,8 +800,8 @@ pub struct ActiveSuggestions { unprocessed_suggestions: VecDeque, /// Fully post-processed suggestions. This is the only collection used by /// fuzzy matching, rendering, and acceptance logic. - processed_suggestions: Vec, - filtered_suggestions: Vec, + pub processed_suggestions: Vec, // TODO think of making htese private + pub filtered_suggestions: Vec, /// 2-D position of the currently-selected suggestion within the grid. /// `selected_col * last_num_rows_per_col + selected_row` gives the 1-D /// index into `filtered_suggestions`. @@ -1181,8 +1181,22 @@ impl ActiveSuggestions { .strip_prefix(&sug.prefix) .unwrap_or(pattern_with_prefix); + log::debug!( + "Fuzzy matching suggestion {:#?} against\n pattern {:?}\n wuc: {:?}", + sug, + pattern, + self.word_under_cursor + ); + // Try the fuzzy matcher first if let Some((score, indices)) = self.fuzzy_matcher.fuzzy_indices(&sug.s, pattern) { + // log::debug!( + // "Fuzzy match for suggestion {:?} against pattern {:?}: score={}, indices={:?}", + // sug.formatted(), + // pattern, + // score, + // indices + // ); return Some(FilteredItem { score, suggestion_idx: idx, @@ -1191,7 +1205,14 @@ impl ActiveSuggestions { } const MAX_PATTERN_LENGTH: usize = 64; + // I've noticed that when the pattern is very long, arinae matcher returns None. + // So here we force it to return a dummy match. if pattern.len() > MAX_PATTERN_LENGTH { + // log::debug!( + // "Pattern {:?} is too long ({} chars), skipping fuzzy match and returning dummy match", + // pattern, + // pattern.len() + // ); return Some(FilteredItem { score: 0, suggestion_idx: idx, @@ -1224,6 +1245,7 @@ impl ActiveSuggestions { .iter() .enumerate() .filter_map(|(idx, sug)| self.fuzzy_match_for_processed(idx, sug)) + // .inspect(|x| log::debug!("Fuzzy match result: idx={}, score={}, matching_indices={:?}", x.suggestion_idx, x.score, x.matching_indices)) .collect(); // Sort by score (descending - higher scores are better matches) diff --git a/src/app/tab_completion.rs b/src/app/tab_completion.rs index 88f7e76..3590cf8 100644 --- a/src/app/tab_completion.rs +++ b/src/app/tab_completion.rs @@ -231,7 +231,7 @@ fn gen_completions_uncomitted( } CompType::FirstWord => { log::debug!("CompType::FirstWord for: {}", word_under_cursor.as_ref()); - let completions = tab_complete_first_word(word_under_cursor.as_ref()); + let completions = tab_complete_first_word(word_under_cursor.as_ref(), word_under_cursor.as_ref()); log::debug!( "CompType::FirstWord found {} completions for prefix: {}", completions.len(), @@ -393,7 +393,7 @@ fn gen_completions_uncomitted( CompType::GlobExpansion => { log::debug!("CompType::GlobExpansion for {}", word_under_cursor.as_ref()); let (completions, comp_res_flags) = - tab_complete_glob_expansion(word_under_cursor.as_ref()); + tab_complete_glob_expansion(word_under_cursor.as_ref(), word_under_cursor.as_ref()); log::debug!( "CompType::GlobExpansion found {} completions for pattern: {}", @@ -461,6 +461,7 @@ fn gen_completions_uncomitted( &(completion_context.word_left_of_cursor().to_string() + "*" + completion_context.word_right_of_cursor()), + word_under_cursor.as_ref() ); log::debug!( @@ -533,7 +534,7 @@ fn filter_out_non_executables(paths: Vec) -> Vec ActiveSuggestionsBuilder { +fn tab_complete_first_word(command: &str, word_under_cursor: &str) -> ActiveSuggestionsBuilder { log::debug!("Generating first word completions for: '{}'", command); if command.is_empty() { return ActiveSuggestionsBuilder::new(); @@ -541,7 +542,7 @@ fn tab_complete_first_word(command: &str) -> ActiveSuggestionsBuilder { if command.starts_with('.') || command.contains('/') || command.starts_with('~') { // Path to executable - let (files, _comp_res_flags) = tab_complete_glob_expansion(&(command.to_string() + "*")); + let (files, _comp_res_flags) = tab_complete_glob_expansion(&(command.to_string() + "*"), word_under_cursor); let executable_files = filter_out_non_executables(files); return ActiveSuggestionsBuilder::from_unprocessed(executable_files); } @@ -604,6 +605,7 @@ fn tab_complete_fuzzy_first_word(command: &str) -> ActiveSuggestionsBuilder { fn tab_complete_with_expanded_pattern( expanded: &PathPatternExpansion, comp_resultflags: bash_funcs::CompletionFlags, + wuc: &str, should_skip_hidden: bool, ) -> Vec { let mut results = Vec::new(); @@ -658,11 +660,7 @@ fn tab_complete_with_expanded_pattern( raw_text: unexpanded, full_path: Some(path), flags: comp_resultflags, - // The glob expansion path already preserves the raw prefix in - // `unexpanded` via PathPatternExpansion; pass "" here so - // into_processed doesn't attempt a second - // prefix split (filename_quoting_desired is false anyway). - word_under_cursor: String::new(), + word_under_cursor: wuc.to_string(), }); } } @@ -673,6 +671,7 @@ fn tab_complete_with_expanded_pattern( fn tab_complete_glob_expansion( pattern: &str, + word_under_cursor: &str, ) -> (Vec, bash_funcs::CompletionFlags) { let mut comp_resultflags = bash_funcs::CompletionFlags::default(); // We will handle it ourselves because the prefix should not be quoted but the found filename should be. @@ -687,7 +686,7 @@ fn tab_complete_glob_expansion( log::debug!("found quote type: {:?}", comp_resultflags.quote_type); let expanded = PathPatternExpansion::new(pattern); - let completions = tab_complete_with_expanded_pattern(&expanded, comp_resultflags, true); + let completions = tab_complete_with_expanded_pattern(&expanded, comp_resultflags, word_under_cursor, true); (completions, comp_resultflags) } @@ -1059,7 +1058,7 @@ impl App<'_> { #[cfg(test)] mod tab_completion_tests { use super::*; - use crate::active_suggestions::ProcessedSuggestion; + use crate::active_suggestions::{FilteredItem, ProcessedSuggestion}; use crate::tab_completion_context::{CompletionContext, get_completion_context}; use crate::text_buffer::TextBuffer; use rusty_fork::rusty_fork_test; @@ -1123,6 +1122,19 @@ mod tab_completion_tests { suggestions } + fn run_to_active_suggestions(buffer: &mut TextBuffer) -> ActiveSuggestions { + crate::logging::init_for_tests_once(); + + let (builder, comp_context) = get_builder_from_buffer(buffer).unwrap(); + let outcome = apply_tab_complete_to_buffer(buffer, &builder, &comp_context.word_under_cursor); + let final_wuc = if let TabCompleteBufferOutcome::Pending { final_wuc } = outcome { + final_wuc + } else { + panic!("Expected pending outcome with suggestions"); + }; + ActiveSuggestions::new(builder, final_wuc, std::time::Duration::from_secs(0)) + } + fn assert_completions(command: &str, expected: &[ProcessedSuggestion]) { let actual = run_completion(command); assert_processed(&actual, expected); @@ -1298,12 +1310,12 @@ mod tab_completion_tests { assert_completions( "mycmd ./", &[ - ProcessedSuggestion::new("./abc/", "", ""), - ProcessedSuggestion::new("./bar.txt", "", " "), - ProcessedSuggestion::new(r"./file\ with\ spaces.txt", "", " "), - ProcessedSuggestion::new("./foo/", "", ""), - ProcessedSuggestion::new(r"./many\ spaces\ here/", "", ""), - ProcessedSuggestion::new("./sym_link_to_foo/", "", ""), + ProcessedSuggestion::new("abc/", "./", ""), + ProcessedSuggestion::new("bar.txt", "./", " "), + ProcessedSuggestion::new(r"file\ with\ spaces.txt", "./", " "), + ProcessedSuggestion::new("foo/", "./", ""), + ProcessedSuggestion::new(r"many\ spaces\ here/", "./", ""), + ProcessedSuggestion::new("sym_link_to_foo/", "./", ""), ], ); } @@ -1500,138 +1512,71 @@ mod tab_completion_tests { #[test] fn fuzzy_matching_with_long_filenames() { cd_to_example_long_filenames_fs(); - let load_time = std::time::Duration::from_millis(0); - - // Read the directory and verify we have the expected filenames - let entries: Vec = std::fs::read_dir(".") - .unwrap() - .filter_map(|e| e.ok()) - .filter_map(|e| { - let path = e.path(); - if path.is_file() { - path.file_name().and_then(|n| n.to_str()).map(|s| s.to_string()) - } else { - None - } - }) - .collect(); - - // Verify we have files of the expected lengths - assert!(entries.iter().any(|s| s.len() == 32), "Missing 32-char filename"); - assert!(entries.iter().any(|s| s.len() == 33), "Missing 33-char filename"); - assert!(entries.iter().any(|s| s.len() == 64), "Missing 64-char filename"); - assert!(entries.iter().any(|s| s.len() == 65), "Missing 65-char filename"); - assert!(entries.iter().any(|s| s.len() == 100), "Missing 100-char filename"); - - // Create ProcessedSuggestions for each filename - let suggestions: Vec = entries - .iter() - .map(|name| ProcessedSuggestion::new(name.clone(), "", " ")) - .collect(); - -// Test 1: Normal pattern (within 32 char limit) - fuzzy matching works - let pattern_normal = "aaa"; - let word_under_cursor_normal = SubString::from_parts(pattern_normal.to_string(), 0); - let builder = ActiveSuggestionsBuilder::from_processed(suggestions.clone()); - let active_sugg = ActiveSuggestions::new(builder, word_under_cursor_normal, load_time); - let filtered_count_normal = active_sugg.filtered_suggestions_len(); - - log::info!( - "Test 1 (normal pattern, 3 chars) - Pattern: 'aaa', Filtered suggestions: {}", - filtered_count_normal - ); - // Should match all files since they all start with 'aaa...' - assert!( - filtered_count_normal > 0, - "Expected fuzzy matches for 'aaa' pattern" - ); - -// Test 2: Pattern exactly at limit (32 chars) - fuzzy matching works - let pattern_at_limit = "a".repeat(32); - let word_under_cursor_32 = SubString::from_parts(pattern_at_limit.clone(), 0); - let builder = ActiveSuggestionsBuilder::from_processed(suggestions.clone()); - let active_sugg = ActiveSuggestions::new(builder, word_under_cursor_32, load_time); - let filtered_count_32 = active_sugg.filtered_suggestions_len(); - - log::info!( - "Test 2 (at limit, 32 chars) - Pattern: 'a'x32, Filtered suggestions: {}", - filtered_count_32 - ); - // At the limit, fuzzy matcher should still work - assert!( - filtered_count_32 > 0, - "Expected fuzzy matches for 32-char pattern (at limit)" - ); - -// Test 3: Pattern slightly over limit (33 chars) - returns dummy items, filtered to 0 - let pattern_over_limit = "a".repeat(33); - let word_under_cursor_33 = SubString::from_parts(pattern_over_limit.clone(), 0); - let builder = ActiveSuggestionsBuilder::from_processed(suggestions.clone()); - let active_sugg = ActiveSuggestions::new(builder, word_under_cursor_33, load_time); - let filtered_count_33 = active_sugg.filtered_suggestions_len(); - log::info!( - "Test 3 (over limit, 33 chars) - Pattern: 'a'x33, Filtered suggestions: {}", - filtered_count_33 - ); - // Over the limit, fuzzy matcher returns None, we return score-0 dummy items, - // which are filtered out, so we should get 0 results - assert_eq!( - filtered_count_33, 0, - "Expected no matches for 33-char pattern (fuzzy matcher limit exceeded)" + let mut buffer = TextBuffer::new_with_cursor("mycmd ./len_61_plus_3/█"); + let active_suggestions = run_to_active_suggestions(&mut buffer); + assert_eq!(buffer.buffer(), "mycmd ./len_61_plus_3/abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_a"); + assert_processed( + &active_suggestions.processed_suggestions, + &[ + ProcessedSuggestion::new( + "abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_aBAR", + "./len_61_plus_3/", + " ", + ), + ProcessedSuggestion::new( + "abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_aFOO", + "./len_61_plus_3/", + " ", + ), + ], ); + assert_eq!(active_suggestions.filtered_suggestions, vec![ + FilteredItem{ + suggestion_idx: 0, + score: 2006, + matching_indices: (0..=60).collect(), + }, + FilteredItem{ + suggestion_idx: 1, + score: 2006, + matching_indices: (0..=60).collect(), + } + ]); - // Test 4: Pattern at 64 chars (well over limit) - let pattern_64 = "a".repeat(64); - let word_under_cursor_64 = SubString::from_parts(pattern_64.clone(), 0); - let builder = ActiveSuggestionsBuilder::from_processed(suggestions.clone()); - let active_sugg = ActiveSuggestions::new(builder, word_under_cursor_64, load_time); - let filtered_count_64 = active_sugg.filtered_suggestions_len(); - - log::info!( - "Test 4 (64 chars) - Pattern: 'a'x64, Filtered suggestions: {}", - filtered_count_64 - ); - // Well over limit, should result in 0 matches - assert_eq!( - filtered_count_64, 0, - "Expected no matches for 64-char pattern (well over limit)" - ); - // Test 5: Pattern at 100 chars - let pattern_100 = "a".repeat(100); - let word_under_cursor_100 = SubString::from_parts(pattern_100.clone(), 0); - let builder = ActiveSuggestionsBuilder::from_processed(suggestions.clone()); - let active_sugg = ActiveSuggestions::new(builder, word_under_cursor_100, load_time); - let filtered_count_100 = active_sugg.filtered_suggestions_len(); - log::info!( - "Test 5 (100 chars) - Pattern: 'a'x100, Filtered suggestions: {}", - filtered_count_100 - ); - // Well over limit, should result in 0 matches - assert_eq!( - filtered_count_100, 0, - "Expected no matches for 100-char pattern (well over limit)" + let mut buffer = TextBuffer::new_with_cursor("mycmd ./len_65_plus_3/█"); + let active_suggestions = run_to_active_suggestions(&mut buffer); + assert_eq!(buffer.buffer(), "mycmd ./len_65_plus_3/abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_"); + assert_processed( + &active_suggestions.processed_suggestions, + &[ + ProcessedSuggestion::new( + "abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_BAR", + "./len_65_plus_3/", + " ", + ), + ProcessedSuggestion::new( + "abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_FOO", + "./len_65_plus_3/", + " ", + ), + ], ); + assert_eq!(active_suggestions.filtered_suggestions, vec![ + FilteredItem{ + suggestion_idx: 0, + score: 0, + matching_indices: vec![], + }, + FilteredItem{ + suggestion_idx: 1, + score: 0, + matching_indices: vec![], + } + ]); - // Test 6: Pattern longer than all files (150 chars) - let pattern_150 = "a".repeat(150); - let word_under_cursor_150 = SubString::from_parts(pattern_150.clone(), 0); - let builder = ActiveSuggestionsBuilder::from_processed(suggestions.clone()); - let active_sugg = ActiveSuggestions::new(builder, word_under_cursor_150, load_time); - let filtered_count_150 = active_sugg.filtered_suggestions_len(); - - log::info!( - "Test 6 (150 chars) - Pattern: 'a'x150, Filtered suggestions: {}", - filtered_count_150 - ); - // Pattern much longer than all files should result in 0 matches - assert_eq!( - filtered_count_150, 0, - "Expected no matches for pattern (len {}) longer than all files", - pattern_150.len() - ); } } } diff --git a/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa b/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa deleted file mode 100644 index 9daeafb..0000000 --- a/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +++ /dev/null @@ -1 +0,0 @@ -test diff --git a/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa b/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa deleted file mode 100644 index 9daeafb..0000000 --- a/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +++ /dev/null @@ -1 +0,0 @@ -test diff --git a/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa b/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa deleted file mode 100644 index 9daeafb..0000000 --- a/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +++ /dev/null @@ -1 +0,0 @@ -test diff --git a/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa b/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa deleted file mode 100644 index 9daeafb..0000000 --- a/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +++ /dev/null @@ -1 +0,0 @@ -test diff --git a/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa b/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa deleted file mode 100644 index 9daeafb..0000000 --- a/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +++ /dev/null @@ -1 +0,0 @@ -test diff --git a/tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbb b/tests/example_long_filenames_fs/len_61_plus_3/abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_aBAR similarity index 100% rename from tests/example_long_filenames_fs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbb rename to tests/example_long_filenames_fs/len_61_plus_3/abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_aBAR diff --git a/tests/example_long_filenames_fs/len_61_plus_3/abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_aFOO b/tests/example_long_filenames_fs/len_61_plus_3/abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_aFOO new file mode 100644 index 0000000..e69de29 From b57e5f5b3b384df48092da586b024ce9ef170688 Mon Sep 17 00:00:00 2001 From: Hal Frigaard <4559349+HalFrgrd@users.noreply.github.com> Date: Sat, 9 May 2026 11:02:23 +0100 Subject: [PATCH 09/10] comments --- src/active_suggestions.rs | 3 +-- src/app/tab_completion.rs | 25 ++++++++++++++++--------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/active_suggestions.rs b/src/active_suggestions.rs index c980eef..1ab515c 100644 --- a/src/active_suggestions.rs +++ b/src/active_suggestions.rs @@ -800,7 +800,7 @@ pub struct ActiveSuggestions { unprocessed_suggestions: VecDeque, /// Fully post-processed suggestions. This is the only collection used by /// fuzzy matching, rendering, and acceptance logic. - pub processed_suggestions: Vec, // TODO think of making htese private + pub processed_suggestions: Vec, // TODO think of making these private pub filtered_suggestions: Vec, /// 2-D position of the currently-selected suggestion within the grid. /// `selected_col * last_num_rows_per_col + selected_row` gives the 1-D @@ -1239,7 +1239,6 @@ impl ActiveSuggestions { self.unprocessed_suggestions.len() ); - self.filtered_suggestions = self .processed_suggestions .iter() diff --git a/src/app/tab_completion.rs b/src/app/tab_completion.rs index 3590cf8..005204e 100644 --- a/src/app/tab_completion.rs +++ b/src/app/tab_completion.rs @@ -231,7 +231,8 @@ fn gen_completions_uncomitted( } CompType::FirstWord => { log::debug!("CompType::FirstWord for: {}", word_under_cursor.as_ref()); - let completions = tab_complete_first_word(word_under_cursor.as_ref(), word_under_cursor.as_ref()); + let completions = + tab_complete_first_word(word_under_cursor.as_ref(), word_under_cursor.as_ref()); log::debug!( "CompType::FirstWord found {} completions for prefix: {}", completions.len(), @@ -392,8 +393,10 @@ fn gen_completions_uncomitted( } CompType::GlobExpansion => { log::debug!("CompType::GlobExpansion for {}", word_under_cursor.as_ref()); - let (completions, comp_res_flags) = - tab_complete_glob_expansion(word_under_cursor.as_ref(), word_under_cursor.as_ref()); + let (completions, comp_res_flags) = tab_complete_glob_expansion( + word_under_cursor.as_ref(), + word_under_cursor.as_ref(), + ); log::debug!( "CompType::GlobExpansion found {} completions for pattern: {}", @@ -461,7 +464,7 @@ fn gen_completions_uncomitted( &(completion_context.word_left_of_cursor().to_string() + "*" + completion_context.word_right_of_cursor()), - word_under_cursor.as_ref() + word_under_cursor.as_ref(), ); log::debug!( @@ -542,7 +545,8 @@ fn tab_complete_first_word(command: &str, word_under_cursor: &str) -> ActiveSugg if command.starts_with('.') || command.contains('/') || command.starts_with('~') { // Path to executable - let (files, _comp_res_flags) = tab_complete_glob_expansion(&(command.to_string() + "*"), word_under_cursor); + let (files, _comp_res_flags) = + tab_complete_glob_expansion(&(command.to_string() + "*"), word_under_cursor); let executable_files = filter_out_non_executables(files); return ActiveSuggestionsBuilder::from_unprocessed(executable_files); } @@ -686,7 +690,8 @@ fn tab_complete_glob_expansion( log::debug!("found quote type: {:?}", comp_resultflags.quote_type); let expanded = PathPatternExpansion::new(pattern); - let completions = tab_complete_with_expanded_pattern(&expanded, comp_resultflags, word_under_cursor, true); + let completions = + tab_complete_with_expanded_pattern(&expanded, comp_resultflags, word_under_cursor, true); (completions, comp_resultflags) } @@ -1126,7 +1131,8 @@ mod tab_completion_tests { crate::logging::init_for_tests_once(); let (builder, comp_context) = get_builder_from_buffer(buffer).unwrap(); - let outcome = apply_tab_complete_to_buffer(buffer, &builder, &comp_context.word_under_cursor); + let outcome = + apply_tab_complete_to_buffer(buffer, &builder, &comp_context.word_under_cursor); let final_wuc = if let TabCompleteBufferOutcome::Pending { final_wuc } = outcome { final_wuc } else { @@ -1513,6 +1519,8 @@ mod tab_completion_tests { fn fuzzy_matching_with_long_filenames() { cd_to_example_long_filenames_fs(); + // Arinae fuzzy matcher stops working at a certain length 64 chars. + // So below that, we can expect fuzzy matching to work. let mut buffer = TextBuffer::new_with_cursor("mycmd ./len_61_plus_3/█"); let active_suggestions = run_to_active_suggestions(&mut buffer); assert_eq!(buffer.buffer(), "mycmd ./len_61_plus_3/abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_a"); @@ -1544,8 +1552,7 @@ mod tab_completion_tests { } ]); - - + // But above that length, fuzzy filtering in active suggestions should just return dummy scores let mut buffer = TextBuffer::new_with_cursor("mycmd ./len_65_plus_3/█"); let active_suggestions = run_to_active_suggestions(&mut buffer); assert_eq!(buffer.buffer(), "mycmd ./len_65_plus_3/abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_abcd_"); From e8d6e4288663b81f837528b68843c6ca15356455 Mon Sep 17 00:00:00 2001 From: Hal Frigaard <4559349+HalFrgrd@users.noreply.github.com> Date: Sat, 9 May 2026 11:05:12 +0100 Subject: [PATCH 10/10] comments --- src/active_suggestions.rs | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/src/active_suggestions.rs b/src/active_suggestions.rs index 1ab515c..1d927ea 100644 --- a/src/active_suggestions.rs +++ b/src/active_suggestions.rs @@ -800,7 +800,7 @@ pub struct ActiveSuggestions { unprocessed_suggestions: VecDeque, /// Fully post-processed suggestions. This is the only collection used by /// fuzzy matching, rendering, and acceptance logic. - pub processed_suggestions: Vec, // TODO think of making these private + pub processed_suggestions: Vec, pub filtered_suggestions: Vec, /// 2-D position of the currently-selected suggestion within the grid. /// `selected_col * last_num_rows_per_col + selected_row` gives the 1-D @@ -1181,22 +1181,8 @@ impl ActiveSuggestions { .strip_prefix(&sug.prefix) .unwrap_or(pattern_with_prefix); - log::debug!( - "Fuzzy matching suggestion {:#?} against\n pattern {:?}\n wuc: {:?}", - sug, - pattern, - self.word_under_cursor - ); - // Try the fuzzy matcher first if let Some((score, indices)) = self.fuzzy_matcher.fuzzy_indices(&sug.s, pattern) { - // log::debug!( - // "Fuzzy match for suggestion {:?} against pattern {:?}: score={}, indices={:?}", - // sug.formatted(), - // pattern, - // score, - // indices - // ); return Some(FilteredItem { score, suggestion_idx: idx, @@ -1208,11 +1194,6 @@ impl ActiveSuggestions { // I've noticed that when the pattern is very long, arinae matcher returns None. // So here we force it to return a dummy match. if pattern.len() > MAX_PATTERN_LENGTH { - // log::debug!( - // "Pattern {:?} is too long ({} chars), skipping fuzzy match and returning dummy match", - // pattern, - // pattern.len() - // ); return Some(FilteredItem { score: 0, suggestion_idx: idx,