diff --git a/src/active_suggestions.rs b/src/active_suggestions.rs index a411e510..04d188d6 100644 --- a/src/active_suggestions.rs +++ b/src/active_suggestions.rs @@ -2158,6 +2158,15 @@ impl ActiveSuggestions { idx: usize, sug: &ProcessedSuggestion, ) -> Option { + if self.auto_started && self.comp_type == tab_completion_context::CompType::GlobExpansion { + // Dont fuzzy filter, always show everything + return Some(FilteredItem { + score: 0, + suggestion_idx: idx, + matching_indices: Vec::new(), + }); + } + let pattern_with_prefix = &self.word_under_cursor.s; let pattern = pattern_with_prefix .strip_prefix(&sug.prefix) diff --git a/src/app/actions/mouse.rs b/src/app/actions/mouse.rs index b8acdf67..3f9ab4aa 100644 --- a/src/app/actions/mouse.rs +++ b/src/app/actions/mouse.rs @@ -257,6 +257,7 @@ impl super::ContextVar for MouseContextVar { Some(Tag::Suggestion(_)) | Some(Tag::TabSuggestion) | Some(Tag::TabCompletionScrollBar { .. }) + | Some(Tag::AutoCompletionTimeInfo) ), MouseContextVar::IsOverFuzzyHistory => matches!( clicked_tag, @@ -337,6 +338,7 @@ fn is_pointer_target_tag(tag: Option, right_click_popup_active: bool) -> bo | Tag::TabCompletionScrollBar { .. } | Tag::FlycompSandboxInfo | Tag::FlycompInfo + | Tag::AutoCompletionTimeInfo | Tag::RightClickCopy | Tag::RightClickCut | Tag::RightClickPaste diff --git a/src/app/mod.rs b/src/app/mod.rs index c37d9245..9917ec71 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -47,9 +47,9 @@ use crate::mouse_state::{MouseState, mouse_state}; use crate::palette::{ButtonState, Palette}; use crate::prompt_manager::PromptManager; use crate::settings::{self, MatrixAnimation, MouseMode, Settings}; -use crate::shell; use crate::shell_integration; use crate::{command_acceptance, dparser}; +use crate::{shell, tab_completion_context}; use flybuffer::{SubString, TextBuffer}; use flash::lexer::TokenKind; @@ -2118,8 +2118,12 @@ impl<'a> App<'a> { Update, } - let get_action = |app: &Self, new_wuc: &SubString| -> Option { - None + let get_action = + |app: &Self, + new_completion_context: &tab_completion_context::CompletionContext<'_>| + -> Option { + let new_wuc = &new_completion_context.word_under_cursor; + None .or_else(|| { mouse_state(|m| m.is_left_button_dragging()) // If we're dragging the mouse, we dont want to have tab completions @@ -2232,6 +2236,12 @@ impl<'a> App<'a> { new_wuc.s ); Some(CompletionAction::Restart { carry_over: true }) + } else if *new_wuc != *current_wuc && active_suggestions.auto_started && new_completion_context.comp_types().contains(&tab_completion_context::CompType::GlobExpansion) { + log::debug!( + "Word under cursor changed ('{:?}') and new completion context contains glob expansion so lets just restart to pick it up.", + new_wuc + ); + Some(CompletionAction::Restart { carry_over: true }) } else if *new_wuc == *current_wuc { log::debug!( "Word under cursor unchanged ('{:?}'), keeping existing tab completion suggestions", @@ -2272,10 +2282,12 @@ impl<'a> App<'a> { _ => None, } }) - }; + }; - let new_wuc = self.get_completion_context().word_under_cursor; - let action = get_action(self, &new_wuc).unwrap_or(CompletionAction::Keep); + let new_completion_context = self.get_completion_context(); + let action = + get_action(self, &new_completion_context).unwrap_or(CompletionAction::Keep); + let new_wuc = new_completion_context.word_under_cursor; match action { CompletionAction::Keep => {} diff --git a/src/app/ui.rs b/src/app/ui.rs index f75fb43b..61982cb7 100644 --- a/src/app/ui.rs +++ b/src/app/ui.rs @@ -909,6 +909,7 @@ impl<'a> App<'a> { self.buffer.buffer(), self.buffer.cursor_byte_pos(), scrollbar_style, + terminal_height, ); } else { Self::render_user_suggestions( @@ -958,6 +959,7 @@ impl<'a> App<'a> { self.buffer.buffer(), self.buffer.cursor_byte_pos(), scrollbar_style, + terminal_height, ); } else { Self::render_user_suggestions( @@ -1718,6 +1720,7 @@ impl<'a> App<'a> { buffer: &str, cursor_byte_pos: usize, scrollbar_style: Style, + terminal_height: u16, ) { let original_buf_len = content.buf.len(); content.newline(); @@ -1785,10 +1788,20 @@ impl<'a> App<'a> { let source_str = format!( "{:.1}ms", - // active_suggestions.comp_type.display_name(), active_suggestions.load_time.as_secs_f32() * 1000.0, ); + let time_hover = + mouse_state(|m| m.last_mouse_over_cell_semantic) == Some(Tag::AutoCompletionTimeInfo); + let source_style = if time_hover { + settings + .colour_palette + .secondary_text() + .add_modifier(Modifier::UNDERLINED) + } else { + settings.colour_palette.secondary_text() + }; + let min_box_width = (unicode_width::UnicodeWidthStr::width(status_prefix.as_str()) + unicode_width::UnicodeWidthStr::width(source_str.as_str()) + 4) @@ -2027,12 +2040,15 @@ impl<'a> App<'a> { let status_line = TaggedLine::from(vec![ TaggedSpan::new( - Span::styled(status_prefix, settings.colour_palette.secondary_text()), + Span::styled( + status_prefix.clone(), + settings.colour_palette.secondary_text(), + ), Tag::TabSuggestion, ), TaggedSpan::new( - Span::styled(source_str, settings.colour_palette.secondary_text()), - Tag::TabSuggestion, + Span::styled(source_str, source_style), + Tag::AutoCompletionTimeInfo, ), ]); @@ -2071,6 +2087,22 @@ impl<'a> App<'a> { if content.buf.len() > final_buf_len { content.buf.truncate(final_buf_len); } + + if time_hover { + let popup_style = settings.colour_palette.normal_text(); + let comptype_msg = active_suggestions.comp_type.display_name(); + let anchor_col = + (x + 2) + unicode_width::UnicodeWidthStr::width(status_prefix.as_str()) as u16; + let anchor_row = y + total_item_rows as u16 + 1; + content.draw_popup( + comptype_msg, + anchor_row + 1, + anchor_col, + terminal_height, + popup_style, + Tag::Normal, + ); + } } fn render_auto_suggestions_loading( @@ -2427,13 +2459,19 @@ mod tests { ); } + static TEST_MOUSE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + #[test] fn test_render_auto_suggestions_selected_wrapping_and_ellipsis() { use crate::active_suggestions::{ ActiveSuggestions, ActiveSuggestionsBuilder, ProcessedSuggestion, SuggestionDescription, }; + use crate::mouse_state::mouse_state; use crate::settings::Settings; + let _guard = TEST_MOUSE_LOCK.lock().unwrap(); + mouse_state(|m| m.last_mouse_over_cell_semantic = None); + let mut settings = Settings::default(); settings.num_suggestion_rows = 5; let mut content = Contents::new(40); @@ -2484,6 +2522,7 @@ mod tests { "", // buffer 0, // cursor_byte_pos Style::default(), // scrollbar_style + 20, // terminal_height ); assert_eq!( @@ -2517,8 +2556,12 @@ mod tests { use crate::active_suggestions::{ ActiveSuggestions, ActiveSuggestionsBuilder, ProcessedSuggestion, SuggestionDescription, }; + use crate::mouse_state::mouse_state; use crate::settings::Settings; + let _guard = TEST_MOUSE_LOCK.lock().unwrap(); + mouse_state(|m| m.last_mouse_over_cell_semantic = None); + let settings = Settings::default(); let mut content = Contents::new(40); @@ -2566,6 +2609,7 @@ mod tests { "", // buffer 0, // cursor_byte_pos Style::default(), // scrollbar_style + 20, // terminal_height ); assert_eq!( @@ -2586,8 +2630,12 @@ mod tests { use crate::active_suggestions::{ ActiveSuggestions, ActiveSuggestionsBuilder, ProcessedSuggestion, SuggestionDescription, }; + use crate::mouse_state::mouse_state; use crate::settings::Settings; + let _guard = TEST_MOUSE_LOCK.lock().unwrap(); + mouse_state(|m| m.last_mouse_over_cell_semantic = None); + let settings = Settings::default(); let mut content = Contents::new(40); @@ -2635,6 +2683,7 @@ mod tests { "", // buffer 0, // cursor_byte_pos Style::default(), // scrollbar_style + 20, // terminal_height ); assert_eq!( @@ -2670,8 +2719,12 @@ mod tests { use crate::active_suggestions::{ ActiveSuggestions, ActiveSuggestionsBuilder, ProcessedSuggestion, }; + use crate::mouse_state::mouse_state; use crate::settings::Settings; + let _guard = TEST_MOUSE_LOCK.lock().unwrap(); + mouse_state(|m| m.last_mouse_over_cell_semantic = None); + let mut settings = Settings::default(); // Set maximum number of suggestion rows to 5 settings.num_suggestion_rows = 5; @@ -2726,6 +2779,7 @@ mod tests { "", // buffer 0, // cursor_byte_pos Style::default(), // scrollbar_style + 20, // terminal_height ); assert_eq!( @@ -2800,6 +2854,7 @@ mod tests { "", // buffer 0, // cursor_byte_pos Style::default(), // scrollbar_style + 20, // terminal_height ); // Nothing was drawn: no popup border on a zero-width terminal. @@ -2846,4 +2901,64 @@ mod tests { "no loading popup should be drawn at zero width", ); } + + #[test] + fn test_render_auto_suggestions_time_hover_shows_comptype_popup() { + use crate::active_suggestions::{ + ActiveSuggestions, ActiveSuggestionsBuilder, ProcessedSuggestion, + }; + use crate::mouse_state::mouse_state; + use crate::settings::Settings; + + let _guard = TEST_MOUSE_LOCK.lock().unwrap(); + let settings = Settings::default(); + let mut content = Contents::new(40); + + let builder = ActiveSuggestionsBuilder { + processed: vec![ + ProcessedSuggestion::new("sug1", "", ""), + ProcessedSuggestion::new("sug2", "", ""), + ], + unprocessed: std::collections::VecDeque::new(), + common_prefix: None, + auto_accept_if_solo: false, + insert_common_prefix: false, + comp_type: crate::tab_completion_context::CompType::FirstWord, + nosort: false, + compspec_was_useful: Some(true), + should_run_flycomp: false, + }; + + let mut active = ActiveSuggestions::new( + builder, + flybuffer::SubString::new("", "").unwrap(), + std::time::Duration::from_millis(0), + true, // auto_started + crate::settings::SuggestionSortOrder::default(), + crate::settings::FuzzyMode::default(), + ); + + // Simulate hovering over the time info tag + mouse_state(|m| m.last_mouse_over_cell_semantic = Some(Tag::AutoCompletionTimeInfo)); + + App::render_auto_suggestions( + &settings, + &mut active, + &mut content, + 40, // width + 20, // rows_left_before_end_of_screen + None, // cursor_pos_maybe + "", // buffer + 0, // cursor_byte_pos + Style::default(), // scrollbar_style + 20, // terminal_height + ); + + // Reset hover state after test + mouse_state(|m| m.last_mouse_over_cell_semantic = None); + + // The popup with "FirstWord" should be drawn below the bottom border! + let lines = content.get_buffer_lines(); + assert!(lines.iter().any(|l| l.contains("FirstWord"))); + } } diff --git a/src/completions/tab_completion.rs b/src/completions/tab_completion.rs index 7933f1f5..1a8d5e80 100644 --- a/src/completions/tab_completion.rs +++ b/src/completions/tab_completion.rs @@ -91,7 +91,7 @@ fn run_comp_spec_completion( match poss_completions { Ok(comp_result) => { - log::debug!( + log::trace!( "Programmable completion results for command: {}", alias_expanded_full_command ); @@ -263,10 +263,6 @@ fn gen_completions_uncomitted( } } CompType::FuzzyFirstWord => { - log::debug!( - "CompType::FuzzyFirstWord for: {}", - word_under_cursor.as_ref() - ); let completions = tab_complete_fuzzy_first_word(word_under_cursor.as_ref()); log::debug!( "CompType::FuzzyFirstWord found {} completions for prefix: {}", @@ -375,7 +371,6 @@ fn gen_completions_uncomitted( } CompType::EnvVariable => { - log::debug!("CompType::EnvVariable for {}", word_under_cursor.as_ref()); let matching_vars = shell::backend().vars_with_prefix(word_under_cursor.as_ref()); log::debug!( "CompType::EnvVariable found {} completions for prefix: {}", @@ -397,10 +392,6 @@ fn gen_completions_uncomitted( } } CompType::HostnameExpansion => { - log::debug!( - "CompType::HostnameExpansion for {}", - word_under_cursor.as_ref() - ); let completions = tab_complete_hostname_expansion(word_under_cursor.as_ref()); log::debug!( "CompType::HostnameExpansion found {} completions for pattern: {}", @@ -415,10 +406,6 @@ fn gen_completions_uncomitted( } } CompType::TildeExpansion => { - log::debug!( - "CompType::TildeExpansion for {}", - word_under_cursor.as_ref() - ); let completions = tab_complete_tilde_expansion(word_under_cursor.as_ref()); log::debug!( "CompType::TildeExpansion found {} completions for pattern: {}", @@ -432,19 +419,41 @@ fn gen_completions_uncomitted( ); } } - CompType::GlobExpansion => { - if auto_started { - log::debug!("Skipping GlobExpansion because auto_started is true"); - continue; + // This shows a preview of what the glob expansion would be + CompType::GlobExpansion if auto_started => { + let (completions, _comp_res_flags) = tab_complete_glob_expansion( + word_under_cursor.as_ref(), + word_under_cursor.as_ref(), + ); + + log::debug!( + "CompType::GlobExpansion auto start found {} completions for pattern: {}", + completions.len(), + word_under_cursor.as_ref() + ); + match completions.as_slice() { + [] => {} + _ => { + return Some( + ActiveSuggestionsBuilder::from_processed( + completions + .into_iter() + .map(|c| c.into_processed()) + .collect::>(), + ) + .with_comp_type(comp_type.clone()), + ); + } } - log::debug!("CompType::GlobExpansion for {}", word_under_cursor.as_ref()); + } + CompType::GlobExpansion => { 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: {}", + "CompType::GlobExpansion manual start found {} completions for pattern: {}", completions.len(), word_under_cursor.as_ref() ); @@ -508,10 +517,6 @@ fn gen_completions_uncomitted( ); continue; } - log::debug!( - "CompType::FilenameExpansion for: {}", - word_under_cursor.as_ref() - ); let (completions, _comp_res_flags) = tab_complete_glob_expansion( &(completion_context.word_left_of_cursor().to_string() + "*" @@ -541,10 +546,6 @@ fn gen_completions_uncomitted( ); continue; } - log::debug!( - "CompType::FuzzyFilenameExpansion for: {}", - word_under_cursor.as_ref() - ); let (completions, _comp_res_flags) = tab_complete_fuzzy_filename(completion_context); @@ -770,7 +771,7 @@ fn tab_complete_glob_expansion( comp_resultflags.filename_completion_desired = true; comp_resultflags.quote_type = shell::find_quote_type(pattern); - log::debug!("found quote type: {:?}", comp_resultflags.quote_type); + log::trace!("found quote type: {:?}", comp_resultflags.quote_type); let expanded = PathPatternExpansion::new(pattern); let completions = diff --git a/src/shell/bash/funcs.rs b/src/shell/bash/funcs.rs index f0c4018c..ebc171ad 100644 --- a/src/shell/bash/funcs.rs +++ b/src/shell/bash/funcs.rs @@ -461,7 +461,7 @@ pub fn useful_compspec_ran(command_word: &str) -> bool { } } if compspec_ptr.is_null() { - log::debug!( + log::trace!( "useful_compspec_ran: no registered compspec found for '{}' (default/fallback)", command_word ); @@ -471,14 +471,14 @@ pub fn useful_compspec_ran(command_word: &str) -> bool { if compspec.funcname.is_null() { if !compspec.command.is_null() { if let Ok(cmd_str) = std::ffi::CStr::from_ptr(compspec.command).to_str() { - log::debug!( + log::trace!( "useful_compspec_ran: registered compspec command for '{}' is: {}", command_word, cmd_str ); } } else { - log::debug!( + log::trace!( "useful_compspec_ran: registered compspec for '{}' has no funcname", command_word ); @@ -487,7 +487,7 @@ pub fn useful_compspec_ran(command_word: &str) -> bool { } let funcname_cstr = std::ffi::CStr::from_ptr(compspec.funcname); if let Ok(funcname_str) = funcname_cstr.to_str() { - log::debug!( + log::trace!( "useful_compspec_ran: registered compspec function for '{}' is: {}", command_word, funcname_str @@ -638,11 +638,6 @@ pub fn run_programmable_completions( // Detect when there was no useful compspec and a dummy one that just returned filenames was used instead let compspec_was_useful = useful_compspec_ran(command_word); - log::debug!( - "run_programmable_completions: useful_compspec_ran for '{}' returned: {}", - command_word, - compspec_was_useful - ); let completion_strings = vec_of_strings_from_char_char_ptr(list_of_strs); @@ -654,7 +649,7 @@ pub fn run_programmable_completions( compspec_was_useful, ); - log::debug!("Programmable completions found: {:#?}", res); + log::trace!("Programmable completions found: {:#?}", res); Ok(res) } diff --git a/src/tag.rs b/src/tag.rs index f416f0b9..6a1d2249 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -31,6 +31,7 @@ pub enum Tag { FlycompDontAsk, FlycompSandboxInfo, FlycompInfo, + AutoCompletionTimeInfo, RightClickCopy, RightClickCut, RightClickPaste,