# Description This change sorts completions for files and directories by the ascending ordering method, related to issue: [#8023](https://github.com/nushell/nushell/issues/8023) Currently the Suggestions are being sorted twice, so it's now following the convention from `completion/base.rs` to match on the `self.get_sort_by()` result. # User-Facing Changes Previously the suggestions were being sorted by the Levenshtein method: ``` /home/rdevenney/projects/open_source/nushell| cd src/ wix/ docs/ tests/ assets/ crates/ docker/ images/ target/ benches/ pkg_mgrs/ .git/ .cargo/ .github/ ``` Now when you tab for autocompletions, they show up in ascending alphabetical order as shown below (with hidden files/folders at the end). ``` /home/rdevenney/projects/open_source/nushell| cd assets/ benches/ crates/ docker/ docs/ images/ pkg_mgrs/ src/ target/ tests/ wix/ .cargo/ .git/ .github/ ``` And when you've already typed a bit of the path: ``` /home/rdevenney/projects/open_source/nushell| cd crates/nu crates/nu-cli/ crates/nu-color-config/ crates/nu-command/ crates/nu-engine/ crates/nu-explore/ crates/nu-glob/ crates/nu-json/ crates/nu-parser/ crates/nu-path/ crates/nu-plugin/ crates/nu-pretty-hex/ crates/nu-protocol/ crates/nu-system/ crates/nu-table/ crates/nu-term-grid/ crates/nu-test-support/ crates/nu-utils/ crates/nu_plugin_custom_values/ crates/nu_plugin_example/ crates/nu_plugin_formats/ crates/nu_plugin_gstat/ crates/nu_plugin_inc/ crates/nu_plugin_python/ crates/nu_plugin_query/ ``` And another for when there are files and directories present: ``` /home/rdevenney/projects/open_source/nushell/crates/nu-cli/src| nvim 02/16/2023 08:22:16 AM commands.rs completions/ config_files.rs eval_file.rs lib.rs menus/ nu_highlight.rs print.rs prompt.rs prompt_update.rs reedline_config.rs repl.rs syntax_highlight.rs util.rs validation.rs ``` # Tests + Formatting Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: [*] `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) [*] `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` to check that you're using the standard code style [*] `cargo test --workspace` to check that all tests pass # After Submitting If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date.
210 lines
6.8 KiB
Rust
210 lines
6.8 KiB
Rust
use crate::completions::{Completer, CompletionOptions};
|
|
use nu_protocol::{
|
|
engine::{EngineState, StateWorkingSet},
|
|
levenshtein_distance, Span,
|
|
};
|
|
use reedline::Suggestion;
|
|
use std::path::{is_separator, Path};
|
|
use std::sync::Arc;
|
|
|
|
use super::SortBy;
|
|
|
|
const SEP: char = std::path::MAIN_SEPARATOR;
|
|
|
|
#[derive(Clone)]
|
|
pub struct FileCompletion {
|
|
engine_state: Arc<EngineState>,
|
|
}
|
|
|
|
impl FileCompletion {
|
|
pub fn new(engine_state: Arc<EngineState>) -> Self {
|
|
Self { engine_state }
|
|
}
|
|
}
|
|
|
|
impl Completer for FileCompletion {
|
|
fn fetch(
|
|
&mut self,
|
|
_: &StateWorkingSet,
|
|
prefix: Vec<u8>,
|
|
span: Span,
|
|
offset: usize,
|
|
_: usize,
|
|
options: &CompletionOptions,
|
|
) -> Vec<Suggestion> {
|
|
let cwd = self.engine_state.current_work_dir();
|
|
let prefix = String::from_utf8_lossy(&prefix).to_string();
|
|
let output: Vec<_> = file_path_completion(span, &prefix, &cwd, options)
|
|
.into_iter()
|
|
.map(move |x| Suggestion {
|
|
value: x.1,
|
|
description: None,
|
|
extra: None,
|
|
span: reedline::Span {
|
|
start: x.0.start - offset,
|
|
end: x.0.end - offset,
|
|
},
|
|
append_whitespace: false,
|
|
})
|
|
.collect();
|
|
|
|
output
|
|
}
|
|
|
|
// Sort results prioritizing the non hidden folders
|
|
fn sort(&self, items: Vec<Suggestion>, prefix: Vec<u8>) -> Vec<Suggestion> {
|
|
let prefix_str = String::from_utf8_lossy(&prefix).to_string();
|
|
|
|
// Sort items
|
|
let mut sorted_items = items;
|
|
|
|
match self.get_sort_by() {
|
|
SortBy::Ascending => {
|
|
sorted_items.sort_by(|a, b| a.value.cmp(&b.value));
|
|
}
|
|
SortBy::LevenshteinDistance => {
|
|
sorted_items.sort_by(|a, b| {
|
|
let a_distance = levenshtein_distance(&prefix_str, &a.value);
|
|
let b_distance = levenshtein_distance(&prefix_str, &b.value);
|
|
a_distance.cmp(&b_distance)
|
|
});
|
|
}
|
|
_ => (),
|
|
}
|
|
|
|
// Separate the results between hidden and non hidden
|
|
let mut hidden: Vec<Suggestion> = vec![];
|
|
let mut non_hidden: Vec<Suggestion> = vec![];
|
|
|
|
for item in sorted_items.into_iter() {
|
|
let item_path = Path::new(&item.value);
|
|
|
|
if let Some(value) = item_path.file_name() {
|
|
if let Some(value) = value.to_str() {
|
|
if value.starts_with('.') {
|
|
hidden.push(item);
|
|
} else {
|
|
non_hidden.push(item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Append the hidden folders to the non hidden vec to avoid creating a new vec
|
|
non_hidden.append(&mut hidden);
|
|
|
|
non_hidden
|
|
}
|
|
}
|
|
|
|
pub fn partial_from(input: &str) -> (String, String) {
|
|
let partial = input.replace('`', "");
|
|
|
|
// If partial is only a word we want to search in the current dir
|
|
let (base, rest) = partial.rsplit_once(is_separator).unwrap_or((".", &partial));
|
|
// On windows, this standardizes paths to use \
|
|
let mut base = base.replace(is_separator, &SEP.to_string());
|
|
|
|
// rsplit_once removes the separator
|
|
base.push(SEP);
|
|
|
|
(base.to_string(), rest.to_string())
|
|
}
|
|
|
|
pub fn file_path_completion(
|
|
span: nu_protocol::Span,
|
|
partial: &str,
|
|
cwd: &str,
|
|
options: &CompletionOptions,
|
|
) -> Vec<(nu_protocol::Span, String)> {
|
|
let original_input = partial;
|
|
let (base_dir_name, partial) = partial_from(partial);
|
|
|
|
let base_dir = nu_path::expand_path_with(&base_dir_name, cwd);
|
|
// This check is here as base_dir.read_dir() with base_dir == "" will open the current dir
|
|
// which we don't want in this case (if we did, base_dir would already be ".")
|
|
if base_dir == Path::new("") {
|
|
return Vec::new();
|
|
}
|
|
|
|
if let Ok(result) = base_dir.read_dir() {
|
|
return result
|
|
.filter_map(|entry| {
|
|
entry.ok().and_then(|entry| {
|
|
let mut file_name = entry.file_name().to_string_lossy().into_owned();
|
|
if matches(&partial, &file_name, options) {
|
|
let mut path = if prepend_base_dir(original_input, &base_dir_name) {
|
|
format!("{base_dir_name}{file_name}")
|
|
} else {
|
|
file_name.to_string()
|
|
};
|
|
|
|
if entry.path().is_dir() {
|
|
path.push(SEP);
|
|
file_name.push(SEP);
|
|
}
|
|
|
|
// Fix files or folders with quotes or hashes
|
|
if path.contains('\'')
|
|
|| path.contains('"')
|
|
|| path.contains(' ')
|
|
|| path.contains('#')
|
|
|| path.contains('(')
|
|
|| path.contains(')')
|
|
|| path.starts_with('0')
|
|
|| path.starts_with('1')
|
|
|| path.starts_with('2')
|
|
|| path.starts_with('3')
|
|
|| path.starts_with('4')
|
|
|| path.starts_with('5')
|
|
|| path.starts_with('6')
|
|
|| path.starts_with('7')
|
|
|| path.starts_with('8')
|
|
|| path.starts_with('9')
|
|
{
|
|
path = format!("`{path}`");
|
|
}
|
|
|
|
Some((span, path))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
})
|
|
.collect();
|
|
}
|
|
|
|
Vec::new()
|
|
}
|
|
|
|
pub fn matches(partial: &str, from: &str, options: &CompletionOptions) -> bool {
|
|
// Check for case sensitive
|
|
if !options.case_sensitive {
|
|
return options
|
|
.match_algorithm
|
|
.matches_str(&from.to_ascii_lowercase(), &partial.to_ascii_lowercase());
|
|
}
|
|
|
|
options.match_algorithm.matches_str(from, partial)
|
|
}
|
|
|
|
/// Returns whether the base_dir should be prepended to the file path
|
|
pub fn prepend_base_dir(input: &str, base_dir: &str) -> bool {
|
|
if base_dir == format!(".{SEP}") {
|
|
// if the current base_dir path is the local folder we only add a "./" prefix if the user
|
|
// input already includes a local folder prefix.
|
|
let manually_entered = {
|
|
let mut chars = input.chars();
|
|
let first_char = chars.next();
|
|
let second_char = chars.next();
|
|
|
|
first_char == Some('.') && second_char.map(is_separator).unwrap_or(false)
|
|
};
|
|
|
|
manually_entered
|
|
} else {
|
|
// always prepend the base dir if it is a subfolder
|
|
true
|
|
}
|
|
}
|