mirror of
https://github.com/nushell/nushell.git
synced 2025-08-11 05:44:22 +02:00
Handle multiple exact matches (#15772)
# Description Fixes #15734. With case-insensitive matching, when completing a file/folder, there can be multiple exact matches. For example, if you have three folders `aa/`, `AA/`, and `aaa/`, `aa/<TAB>` should match all of them. But, as reported in #15734, when using prefix matching, only `AA/` will be shown. This is because when there's an exact match in prefix match mode, we only show the first exact match. There are two options for fixing this: - Show all matched suggestions (`aa/`, `AA/`, and `aaa/`) - I chose this option - Show only the suggestions that matched exactly (`aa/` and `AA/`) but not others (`aaa/`) - This felt unintuitive # User-Facing Changes As mentioned above, when: - you have multiple folders with the same name but in different cases - and you're using prefix matching - and you're using case-insensitive matching - and you type in the name of one of these folders exactly then you'll be suggested every folder matching the typed text, rather than just exact matches # Tests + Formatting I added a test that doesn't run on Windows or MacOS (to avoid case-insensitive filesystems). While adding this test, I felt like using `Playground` rather than adding files to `tests/fixtures`. To make this easier, I refactored the `new_*_engine()` helpers in `completion_helpers.rs` a bit. There was quite a bit of code duplication there. # After Submitting N/A
This commit is contained in:
@ -39,8 +39,10 @@ fn complete_rec(
|
||||
isdir: bool,
|
||||
enable_exact_match: bool,
|
||||
) -> Vec<PathBuiltFromString> {
|
||||
let has_more = !partial.is_empty() && (partial.len() > 1 || isdir);
|
||||
|
||||
if let Some((&base, rest)) = partial.split_first() {
|
||||
if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) {
|
||||
if base.chars().all(|c| c == '.') && has_more {
|
||||
let built_paths: Vec<_> = built_paths
|
||||
.iter()
|
||||
.map(|built| {
|
||||
@ -64,6 +66,9 @@ fn complete_rec(
|
||||
let prefix = partial.first().unwrap_or(&"");
|
||||
let mut matcher = NuMatcher::new(prefix, options);
|
||||
|
||||
let mut exact_match = None;
|
||||
// Only relevant for case insensitive matching
|
||||
let mut multiple_exact_matches = false;
|
||||
for built in built_paths {
|
||||
let mut path = built.cwd.clone();
|
||||
for part in &built.parts {
|
||||
@ -83,48 +88,56 @@ fn complete_rec(
|
||||
built.isdir = entry_isdir && !entry.path().is_symlink();
|
||||
|
||||
if !want_directory || entry_isdir {
|
||||
matcher.add(entry_name.clone(), (entry_name, built));
|
||||
if enable_exact_match && !multiple_exact_matches && has_more {
|
||||
let matches = if options.case_sensitive {
|
||||
entry_name.eq(prefix)
|
||||
} else {
|
||||
entry_name.eq_ignore_case(prefix)
|
||||
};
|
||||
if matches {
|
||||
if exact_match.is_none() {
|
||||
exact_match = Some(built.clone());
|
||||
} else {
|
||||
multiple_exact_matches = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matcher.add(entry_name, built);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut completions = vec![];
|
||||
for (entry_name, built) in matcher.results() {
|
||||
match partial.split_first() {
|
||||
Some((base, rest)) => {
|
||||
// We use `isdir` to confirm that the current component has
|
||||
// at least one next component or a slash.
|
||||
// Serves as confirmation to ignore longer completions for
|
||||
// components in between.
|
||||
if !rest.is_empty() || isdir {
|
||||
// Don't show longer completions if we have an exact match (#13204, #14794)
|
||||
let exact_match = enable_exact_match
|
||||
&& (if options.case_sensitive {
|
||||
entry_name.eq(base)
|
||||
} else {
|
||||
entry_name.eq_ignore_case(base)
|
||||
});
|
||||
completions.extend(complete_rec(
|
||||
rest,
|
||||
&[built],
|
||||
options,
|
||||
want_directory,
|
||||
isdir,
|
||||
exact_match,
|
||||
));
|
||||
if exact_match {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
completions.push(built);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
completions.push(built);
|
||||
}
|
||||
// Don't show longer completions if we have a single exact match (#13204, #14794)
|
||||
if !multiple_exact_matches {
|
||||
if let Some(built) = exact_match {
|
||||
return complete_rec(
|
||||
&partial[1..],
|
||||
&[built],
|
||||
options,
|
||||
want_directory,
|
||||
isdir,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
completions
|
||||
|
||||
if has_more {
|
||||
let mut completions = vec![];
|
||||
for built in matcher.results() {
|
||||
completions.extend(complete_rec(
|
||||
&partial[1..],
|
||||
&[built],
|
||||
options,
|
||||
want_directory,
|
||||
isdir,
|
||||
false,
|
||||
));
|
||||
}
|
||||
completions
|
||||
} else {
|
||||
matcher.results()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
Reference in New Issue
Block a user