Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
target/
*.so
*.gif
*.svg
docker_build/
1 change: 1 addition & 0 deletions src/active_suggestions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
90 changes: 66 additions & 24 deletions src/app/tab_completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}
}

Expand Down Expand Up @@ -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: {}",
Expand Down Expand Up @@ -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: {}",
Expand All @@ -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: {}",
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -636,8 +635,8 @@ fn tab_complete_with_expanded_pattern(

fn tab_complete_glob_expansion(
pattern: &str,
mut comp_resultflags: bash_funcs::CompletionFlags,
) -> Vec<UnprocessedSuggestion> {
) -> (Vec<UnprocessedSuggestion>, 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<TAB> should expand to:
// my_command $PWD/file\ with\ spaces.txt
Expand All @@ -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
Expand All @@ -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<UnprocessedSuggestion> {
) -> (Vec<UnprocessedSuggestion>, 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.

Expand All @@ -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
Expand Down Expand Up @@ -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<ProcessedSuggestion> {
Expand Down Expand Up @@ -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<ProcessedSuggestion> {
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 {
Expand Down Expand Up @@ -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) -------

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down
2 changes: 0 additions & 2 deletions src/content_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -699,14 +699,12 @@ pub fn easing_animation_frames(easing: CursorEasing) -> Vec<Vec<Span<'static>>>

#[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,
Expand Down
17 changes: 17 additions & 0 deletions src/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -82,6 +84,8 @@ impl Log for MemoryLogger {

static LOGGER: OnceLock<MemoryLogger> = 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);
Expand All @@ -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)
Expand Down
36 changes: 22 additions & 14 deletions src/tab_completion_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
}

Expand All @@ -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)
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
]
);
Expand Down Expand Up @@ -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
]
);
Expand Down Expand Up @@ -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
]
);
Expand All @@ -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
]
);
Expand Down
Loading