diff --git a/.gitignore b/.gitignore index 87ee1eeb..9cf0fff3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ target/ *.so *.gif +*.svg docker_build/ diff --git a/src/active_suggestions.rs b/src/active_suggestions.rs index d7c8db97..66fb544e 100644 --- a/src/active_suggestions.rs +++ b/src/active_suggestions.rs @@ -676,6 +676,7 @@ impl ActiveSuggestionsBuilder { } /// Append a single already-processed suggestion. + #[allow(dead_code)] pub fn push_processed(&mut self, sug: ProcessedSuggestion) { self.processed.push(sug); } diff --git a/src/app/tab_completion.rs b/src/app/tab_completion.rs index 17496153..2283807e 100644 --- a/src/app/tab_completion.rs +++ b/src/app/tab_completion.rs @@ -222,8 +222,6 @@ fn gen_completions_uncomitted( let word_under_cursor = &completion_context.word_under_cursor; - let mut comp_res_flags = bash_funcs::CompletionFlags::default(); - for comp_type in &completion_context.comp_types { log::debug!("Processing completion type: {:?}", comp_type); match comp_type { @@ -332,7 +330,14 @@ fn gen_completions_uncomitted( }) .collect(); builder = builder.with_auto_accept_if_solo(false); - return Some(builder); + log::debug!( + "CompType::FuzzyCommandComp found {} completions for pattern: {}", + builder.len(), + pattern + ); + if !builder.is_empty() { + return Some(builder); + } } } @@ -368,8 +373,8 @@ fn gen_completions_uncomitted( } tab_completion_context::CompType::GlobExpansion => { log::debug!("CompType::GlobExpansion for {}", word_under_cursor.as_ref()); - let completions = - tab_complete_glob_expansion(word_under_cursor.as_ref(), comp_res_flags); + let (completions, comp_res_flags) = + tab_complete_glob_expansion(word_under_cursor.as_ref()); log::debug!( "CompType::GlobExpansion found {} completions for pattern: {}", @@ -425,10 +430,8 @@ fn gen_completions_uncomitted( "CompType::FilenameExpansion for: {}", word_under_cursor.as_ref() ); - let completions = tab_complete_glob_expansion( - &(word_under_cursor.as_ref().to_string() + "*"), - comp_res_flags, - ); + let (completions, _comp_res_flags) = + tab_complete_glob_expansion(&(word_under_cursor.as_ref().to_string() + "*")); log::debug!( "CompType::FilenameExpansion found {} completions for pattern: {}", @@ -444,8 +447,8 @@ fn gen_completions_uncomitted( "CompType::FuzzyFilenameExpansion for: {}", word_under_cursor.as_ref() ); - let completions = - tab_complete_fuzzy_filename(word_under_cursor.as_ref(), comp_res_flags); + let (completions, _comp_res_flags) = + tab_complete_fuzzy_filename(word_under_cursor.as_ref()); log::debug!( "CompType::FuzzyFilenameExpansion found {} completions for pattern: {}", @@ -500,10 +503,7 @@ fn tab_complete_first_word(command: &str) -> ActiveSuggestionsBuilder { if command.starts_with('.') || command.contains('/') || command.starts_with('~') { // Path to executable - let files = tab_complete_glob_expansion( - &(command.to_string() + "*"), - bash_funcs::CompletionFlags::default(), - ); + let (files, _comp_res_flags) = tab_complete_glob_expansion(&(command.to_string() + "*")); let executable_files = filter_out_non_executables(files); return ActiveSuggestionsBuilder::from_unprocessed(executable_files); } @@ -532,8 +532,7 @@ fn tab_complete_fuzzy_first_word(command: &str) -> ActiveSuggestionsBuilder { } if command.starts_with('.') || command.contains('/') || command.starts_with('~') { - let fuzzy_files = - tab_complete_fuzzy_filename(command, bash_funcs::CompletionFlags::default()); + let (fuzzy_files, _comp_res_flags) = tab_complete_fuzzy_filename(command); let executable_files = filter_out_non_executables(fuzzy_files); return ActiveSuggestionsBuilder::from_unprocessed(executable_files); } @@ -636,8 +635,8 @@ fn tab_complete_with_expanded_pattern( fn tab_complete_glob_expansion( pattern: &str, - mut comp_resultflags: bash_funcs::CompletionFlags, -) -> Vec { +) -> (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. // e.g. my_command $PWD/fi should expand to: // my_command $PWD/file\ with\ spaces.txt @@ -650,8 +649,9 @@ 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); - tab_complete_with_expanded_pattern(&expanded, comp_resultflags, true) + (completions, comp_resultflags) } /// List all files in the directory implied by `word_under_cursor` and return @@ -662,8 +662,8 @@ fn tab_complete_glob_expansion( /// but the fuzzy matcher will. fn tab_complete_fuzzy_filename( word_under_cursor: &str, - mut comp_res_flags: bash_funcs::CompletionFlags, -) -> Vec { +) -> (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. @@ -679,7 +679,7 @@ fn tab_complete_fuzzy_filename( // Nothing to fuzzy-match against — let the caller fall through. if filename_fragment.is_empty() { - return vec![]; + return (vec![], comp_res_flags); } // Set up flags for glob expansion @@ -715,7 +715,9 @@ fn tab_complete_fuzzy_filename( // 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()); - scored.into_iter().map(|(_, sug)| sug).collect() + let completions = scored.into_iter().map(|(_, sug)| sug).collect(); + + (completions, comp_res_flags) } fn tab_complete_tilde_expansion(pattern: &str) -> Vec { @@ -944,6 +946,7 @@ mod tab_completion_tests { /// string), drain anything still queued, then return the processed /// suggestions sorted by `s` for stable comparison. fn run_completion(command: &str) -> Vec { + crate::logging::init_for_tests_once(); let buffer = TextBuffer::new(command); let comp_context = get_completion_context(buffer.buffer(), buffer.cursor_byte_pos()); let Some(builder) = gen_completions_internal(&comp_context) else { @@ -990,6 +993,11 @@ mod tab_completion_tests { std::env::set_current_dir(&dir).unwrap_or_else(|e| panic!("cd {dir}: {e}")); } + fn cd_to_example_glob_fs() { + let dir = find_test_fixture_dir("example_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) ------- @@ -1033,6 +1041,26 @@ mod tab_completion_tests { } } + // ------- dummy git completion fuzzy matching + /// This tests the [crate::tab_completion_context::CompType::FuzzyCommandComp] branch where we re-run the + #[test] + fn git_commit_fuzzy_command_comp() { + cd_to_example_fs(); + let actual = run_completion("git cmomit"); // Typo of commit + let names: Vec<&str> = actual.iter().map(|s| s.s.as_str()).collect(); + for flag in ["commit"] { + assert!(names.contains(&flag), "expected {flag} in {:?}", names); + } + } + + #[test] + fn git_commit_fuzzy_command_comp_fallback_if_not_found() { + cd_to_example_fs(); + let actual = run_completion("git symlinktfoo"); // This one should fall back to filenames + assert_eq!(actual.len(), 1); + assert_eq!(actual[0].s, "sym_link_to_foo/"); + } + // ------- alias expansion (find_alias / get_all_aliases) ---------- #[test] @@ -1104,6 +1132,20 @@ mod tab_completion_tests { ); } + + #[test] + fn globbing_test_1() { + cd_to_example_glob_fs(); + assert_completions( + "mycmd bar*", + &[ProcessedSuggestion::new( + "bar1 bar2 bar3 ", + "", + "", + )], + ); + } + // ------- finish_tab_complete (auto-accept solo) ------------------ #[test] diff --git a/src/content_utils.rs b/src/content_utils.rs index e1d31bd9..06692a0c 100644 --- a/src/content_utils.rs +++ b/src/content_utils.rs @@ -699,14 +699,12 @@ pub fn easing_animation_frames(easing: CursorEasing) -> Vec>> #[derive(Debug, Clone, Copy)] pub enum FuzzyMatchThreshold { - None, Medium, High, } fn fuzzy_pattern_score_threshold(pattern_len: usize, threshold: FuzzyMatchThreshold) -> i64 { match threshold { - FuzzyMatchThreshold::None => 0, FuzzyMatchThreshold::Medium => match pattern_len { 0..1 => 0, 1..3 => 10, diff --git a/src/logging.rs b/src/logging.rs index a77d817f..5af2b7bb 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -4,6 +4,8 @@ use log::{LevelFilter, Log, Metadata, Record}; use std::collections::VecDeque; use std::fs::OpenOptions; use std::io::Write; +#[cfg(test)] +use std::sync::Once; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Mutex, OnceLock}; @@ -82,6 +84,8 @@ impl Log for MemoryLogger { static LOGGER: OnceLock = OnceLock::new(); static TERMINAL_STREAMING: AtomicBool = AtomicBool::new(false); +#[cfg(test)] +static TEST_LOG_INIT: Once = Once::new(); pub fn init() -> Result<()> { let logger = LOGGER.get_or_init(MemoryLogger::new); @@ -97,6 +101,19 @@ pub fn init() -> Result<()> { } } +#[cfg(test)] +pub fn init_for_tests_once() { + TEST_LOG_INIT.call_once(|| { + let _ = init(); + + let previous_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info| { + print_logs_stderr(); + previous_hook(panic_info); + })); + }); +} + /// Returns true if `flyline log stream terminal` has been configured. pub fn is_terminal_streaming() -> bool { TERMINAL_STREAMING.load(Ordering::Relaxed) diff --git a/src/tab_completion_context.rs b/src/tab_completion_context.rs index f71a0fde..2843c72e 100644 --- a/src/tab_completion_context.rs +++ b/src/tab_completion_context.rs @@ -126,9 +126,6 @@ impl<'a> CompletionContext<'a> { comp_types.push(CompType::CommandComp { command_word: command_word.clone(), }); - if !wuc_looks_like_path && !wuc_looks_like_env_var { - comp_types.push(CompType::FuzzyCommandComp { command_word }); - } } if wuc_looks_like_env_var { @@ -139,6 +136,16 @@ impl<'a> CompletionContext<'a> { comp_types.push(CompType::GlobExpansion); } else { comp_types.push(CompType::FilenameExpansion); + + for comp_type in &comp_types { + if !wuc_looks_like_path && let CompType::CommandComp { command_word } = comp_type { + comp_types.push(CompType::FuzzyCommandComp { + command_word: command_word.clone(), + }); + break; + } + } + comp_types.push(CompType::FuzzyFilenameExpansion); } @@ -152,6 +159,7 @@ impl<'a> CompletionContext<'a> { &context.as_ref()[..end] } + #[cfg(test)] pub fn context_until_cursor(&self) -> &str { Self::context_until_cursor_for(&self.context, self.cursor_byte_pos) } @@ -264,13 +272,13 @@ pub fn get_completion_context<'a>( let context_tokens = parser.get_current_command_tokens(); - if cfg!(test) { - println!("Context tokens:"); - dbg!(cursor_byte_pos); - for t in context_tokens.iter() { - println!("{:#?} byte_range={:?}\n", t, t.token.byte_range()); - } - } + // if cfg!(test) { + // println!("Context tokens:"); + // dbg!(cursor_byte_pos); + // for t in context_tokens.iter() { + // println!("{:#?} byte_range={:?}\n", t, t.token.byte_range()); + // } + // } // first try and find a non whitespace token that inclusivly contains the cursor. // if there is one, that is the word under the cursor. @@ -707,10 +715,10 @@ mod tests { CompType::CommandComp { command_word: "echo".to_string() }, + CompType::FilenameExpansion, CompType::FuzzyCommandComp { command_word: "echo".to_string() }, - CompType::FilenameExpansion, CompType::FuzzyFilenameExpansion ] ); @@ -1029,10 +1037,10 @@ mod tests { CompType::CommandComp { command_word: "diff".to_string() }, + CompType::FilenameExpansion, CompType::FuzzyCommandComp { command_word: "diff".to_string() }, - CompType::FilenameExpansion, CompType::FuzzyFilenameExpansion ] ); @@ -1477,10 +1485,10 @@ mod tests { CompType::CommandComp { command_word: "cd".to_string() }, + CompType::FilenameExpansion, CompType::FuzzyCommandComp { command_word: "cd".to_string() }, - CompType::FilenameExpansion, CompType::FuzzyFilenameExpansion ] ); @@ -1497,10 +1505,10 @@ mod tests { CompType::CommandComp { command_word: "echo".to_string() }, + CompType::FilenameExpansion, CompType::FuzzyCommandComp { command_word: "echo".to_string() }, - CompType::FilenameExpansion, CompType::FuzzyFilenameExpansion ] ); diff --git a/src/text_buffer.rs b/src/text_buffer.rs index 2cd65c4b..0f4ed835 100644 --- a/src/text_buffer.rs +++ b/src/text_buffer.rs @@ -1841,28 +1841,9 @@ impl SnapshotManager { mod test_undo_redo { use super::*; - use log::{LevelFilter, Log, Metadata, Record}; - - fn setup_logging() { - struct StdoutLogger; - impl Log for StdoutLogger { - fn enabled(&self, _metadata: &Metadata) -> bool { - true - } - fn log(&self, record: &Record) { - if self.enabled(record.metadata()) { - println!("[{}] {}", record.level(), record.args()); - } - } - fn flush(&self) {} - } - static LOGGER: StdoutLogger = StdoutLogger; - let _ = log::set_logger(&LOGGER).map(|()| log::set_max_level(LevelFilter::Debug)); - } - #[test] fn undo_stack() { - setup_logging(); + crate::logging::init_for_tests_once(); let snap = |s: &str| Snapshot::new(s, 0, None); @@ -1900,7 +1881,7 @@ mod test_undo_redo { #[test] fn undo_redo_basic() { - setup_logging(); + crate::logging::init_for_tests_once(); let mut tb = TextBuffer::new("Hello"); tb.insert_str(" World"); println!("{}", tb.debug_undo_stack()); @@ -1915,7 +1896,7 @@ mod test_undo_redo { #[test] fn undo_redo_multiple_steps() { - setup_logging(); + crate::logging::init_for_tests_once(); let mut tb = TextBuffer::new("Start"); tb.insert_str(" One"); tb.insert_str(" Two"); @@ -1937,7 +1918,7 @@ mod test_undo_redo { #[test] fn undo_and_start_new_edit() { - setup_logging(); + crate::logging::init_for_tests_once(); let mut tb = TextBuffer::new("Base"); tb.insert_str(" Edit1"); tb.insert_str(" Edit2"); @@ -1957,7 +1938,7 @@ mod test_undo_redo { #[test] fn undo_replace_word_under_cursor() { - setup_logging(); + crate::logging::init_for_tests_once(); let mut tb = TextBuffer::new("The quick brown fox"); let word = { let i = tb.buffer().find("quick").unwrap(); @@ -1977,7 +1958,7 @@ mod test_undo_redo { #[test] fn undo_restores_selection_after_delete() { - setup_logging(); + crate::logging::init_for_tests_once(); let mut tb = TextBuffer::new("Hello World"); // Select "World" let start = tb.buffer().find("World").unwrap(); @@ -2003,7 +1984,7 @@ mod test_undo_redo { #[test] fn selection_change_does_not_create_snapshot() { - setup_logging(); + crate::logging::init_for_tests_once(); let mut tb = TextBuffer::new("Hello World"); tb.insert_str("!"); assert_eq!(tb.buffer(), "Hello World!"); diff --git a/tests/example_glob_fs/bar1 b/tests/example_glob_fs/bar1 new file mode 100644 index 00000000..e69de29b diff --git a/tests/example_glob_fs/bar2 b/tests/example_glob_fs/bar2 new file mode 100644 index 00000000..e69de29b diff --git a/tests/example_glob_fs/bar3 b/tests/example_glob_fs/bar3 new file mode 100644 index 00000000..e69de29b