forked from extern/nushell
Fish-like completions for nested directories (#10543)
<!-- if this PR closes one or more issues, you can automatically link the PR with them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g. - this PR should close #5683 you can also mention related issues, PRs or discussions! --> # Description <!-- Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes. Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience. --> This PR allows tab completion for nested directories while only specifying a part of the directory names. To illustrate this, if I type `tar/de/inc` and hit tab, it autocompletes to `./target/debug/incremental`. # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> Nested paths can be tab completed by typing lesser characters. # 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` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> Tests cases are added. # 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. -->
This commit is contained in:
parent
eeade99452
commit
5c15a4dd6e
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2741,6 +2741,7 @@ dependencies = [
|
|||||||
"nu-test-support",
|
"nu-test-support",
|
||||||
"nu-utils",
|
"nu-utils",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"pathdiff",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"reedline",
|
"reedline",
|
||||||
"rstest",
|
"rstest",
|
||||||
|
@ -36,6 +36,7 @@ log = "0.4"
|
|||||||
miette = { version = "5.10", features = ["fancy-no-backtrace"] }
|
miette = { version = "5.10", features = ["fancy-no-backtrace"] }
|
||||||
once_cell = "1.18"
|
once_cell = "1.18"
|
||||||
percent-encoding = "2"
|
percent-encoding = "2"
|
||||||
|
pathdiff = "0.2"
|
||||||
sysinfo = "0.29"
|
sysinfo = "0.29"
|
||||||
unicode-segmentation = "1.10"
|
unicode-segmentation = "1.10"
|
||||||
uuid = { version = "1.4.1", features = ["v4"] }
|
uuid = { version = "1.4.1", features = ["v4"] }
|
||||||
|
136
crates/nu-cli/src/completions/completion_common.rs
Normal file
136
crates/nu-cli/src/completions/completion_common.rs
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
use crate::completions::{matches, CompletionOptions};
|
||||||
|
use nu_path::home_dir;
|
||||||
|
use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP};
|
||||||
|
|
||||||
|
fn complete_rec(
|
||||||
|
partial: &[String],
|
||||||
|
cwd: &Path,
|
||||||
|
options: &CompletionOptions,
|
||||||
|
dir: bool,
|
||||||
|
isdir: bool,
|
||||||
|
) -> Vec<PathBuf> {
|
||||||
|
let mut completions = vec![];
|
||||||
|
|
||||||
|
if let Ok(result) = cwd.read_dir() {
|
||||||
|
for entry in result.filter_map(|e| e.ok()) {
|
||||||
|
let entry_name = entry.file_name().to_string_lossy().into_owned();
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if !dir || path.is_dir() {
|
||||||
|
match partial.first() {
|
||||||
|
Some(base) if matches(base, &entry_name, options) => {
|
||||||
|
let partial = &partial[1..];
|
||||||
|
if !partial.is_empty() || isdir {
|
||||||
|
completions.extend(complete_rec(partial, &path, options, dir, isdir))
|
||||||
|
} else {
|
||||||
|
completions.push(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => completions.push(path),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
completions
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OriginalCwd {
|
||||||
|
None,
|
||||||
|
Home(PathBuf),
|
||||||
|
Some(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OriginalCwd {
|
||||||
|
fn apply(&self, p: &Path) -> String {
|
||||||
|
let mut ret = match self {
|
||||||
|
Self::None => p.to_string_lossy().into_owned(),
|
||||||
|
Self::Some(base) => pathdiff::diff_paths(p, base)
|
||||||
|
.unwrap_or(p.to_path_buf())
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned(),
|
||||||
|
Self::Home(home) => match p.strip_prefix(home) {
|
||||||
|
Ok(suffix) => format!("~{}{}", SEP, suffix.to_string_lossy()),
|
||||||
|
_ => p.to_string_lossy().into_owned(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if p.is_dir() {
|
||||||
|
ret.push(SEP);
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn complete_item(
|
||||||
|
want_directory: bool,
|
||||||
|
span: nu_protocol::Span,
|
||||||
|
partial: &str,
|
||||||
|
cwd: &str,
|
||||||
|
options: &CompletionOptions,
|
||||||
|
) -> Vec<(nu_protocol::Span, String)> {
|
||||||
|
let isdir = partial.ends_with(is_separator);
|
||||||
|
let cwd_pathbuf = Path::new(cwd).to_path_buf();
|
||||||
|
let mut original_cwd = OriginalCwd::None;
|
||||||
|
let mut components = Path::new(partial).components().peekable();
|
||||||
|
let mut cwd = match components.peek().cloned() {
|
||||||
|
Some(c @ Component::Prefix(..)) => {
|
||||||
|
// windows only by definition
|
||||||
|
components.next();
|
||||||
|
if let Some(Component::RootDir) = components.peek().cloned() {
|
||||||
|
components.next();
|
||||||
|
};
|
||||||
|
[c, Component::RootDir].iter().collect()
|
||||||
|
}
|
||||||
|
Some(c @ Component::RootDir) => {
|
||||||
|
components.next();
|
||||||
|
PathBuf::from(c.as_os_str())
|
||||||
|
}
|
||||||
|
Some(Component::Normal(home)) if home.to_string_lossy() == "~" => {
|
||||||
|
components.next();
|
||||||
|
original_cwd = OriginalCwd::Home(home_dir().unwrap_or(cwd_pathbuf.clone()));
|
||||||
|
home_dir().unwrap_or(cwd_pathbuf)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
original_cwd = OriginalCwd::Some(cwd_pathbuf.clone());
|
||||||
|
cwd_pathbuf
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut partial = vec![];
|
||||||
|
|
||||||
|
for component in components {
|
||||||
|
match component {
|
||||||
|
Component::Prefix(..) => unreachable!(),
|
||||||
|
Component::RootDir => unreachable!(),
|
||||||
|
Component::CurDir => {}
|
||||||
|
Component::ParentDir => {
|
||||||
|
if partial.pop().is_none() {
|
||||||
|
cwd.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component::Normal(c) => {
|
||||||
|
partial.push(c.to_string_lossy().into_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
complete_rec(partial.as_slice(), &cwd, options, want_directory, isdir)
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| (span, escape_path(original_cwd.apply(&p), want_directory)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix files or folders with quotes or hashes
|
||||||
|
pub fn escape_path(path: String, dir: bool) -> String {
|
||||||
|
let filename_contaminated = !dir
|
||||||
|
&& path.contains([
|
||||||
|
'\'', '"', ' ', '#', '(', ')', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||||
|
]);
|
||||||
|
let dirname_contaminated = dir && path.contains(['\'', '"', ' ', '#']);
|
||||||
|
if filename_contaminated || dirname_contaminated {
|
||||||
|
format!("`{path}`")
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,13 @@
|
|||||||
use crate::completions::{matches, Completer, CompletionOptions};
|
use crate::completions::{completion_common::complete_item, Completer, CompletionOptions};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{EngineState, StateWorkingSet},
|
engine::{EngineState, StateWorkingSet},
|
||||||
levenshtein_distance, Span,
|
levenshtein_distance, Span,
|
||||||
};
|
};
|
||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
use std::fs;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::{partial_from, prepend_base_dir, SortBy};
|
use super::SortBy;
|
||||||
|
|
||||||
const SEP: char = std::path::MAIN_SEPARATOR;
|
const SEP: char = std::path::MAIN_SEPARATOR;
|
||||||
|
|
||||||
@ -33,23 +32,27 @@ impl Completer for DirectoryCompletion {
|
|||||||
_: usize,
|
_: usize,
|
||||||
options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
) -> Vec<Suggestion> {
|
) -> Vec<Suggestion> {
|
||||||
let cwd = self.engine_state.current_work_dir();
|
|
||||||
let partial = String::from_utf8_lossy(&prefix).to_string();
|
let partial = String::from_utf8_lossy(&prefix).to_string();
|
||||||
|
|
||||||
// Filter only the folders
|
// Filter only the folders
|
||||||
let output: Vec<_> = directory_completion(span, &partial, &cwd, options)
|
let output: Vec<_> = directory_completion(
|
||||||
.into_iter()
|
span,
|
||||||
.map(move |x| Suggestion {
|
&partial,
|
||||||
value: x.1,
|
&self.engine_state.current_work_dir(),
|
||||||
description: None,
|
options,
|
||||||
extra: None,
|
)
|
||||||
span: reedline::Span {
|
.into_iter()
|
||||||
start: x.0.start - offset,
|
.map(move |x| Suggestion {
|
||||||
end: x.0.end - offset,
|
value: x.1,
|
||||||
},
|
description: None,
|
||||||
append_whitespace: false,
|
extra: None,
|
||||||
})
|
span: reedline::Span {
|
||||||
.collect();
|
start: x.0.start - offset,
|
||||||
|
end: x.0.end - offset,
|
||||||
|
},
|
||||||
|
append_whitespace: false,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
@ -111,60 +114,5 @@ pub fn directory_completion(
|
|||||||
cwd: &str,
|
cwd: &str,
|
||||||
options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
) -> Vec<(nu_protocol::Span, String)> {
|
) -> Vec<(nu_protocol::Span, String)> {
|
||||||
let original_input = partial;
|
complete_item(true, span, partial, cwd, options)
|
||||||
|
|
||||||
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| {
|
|
||||||
if let Ok(metadata) = fs::metadata(entry.path()) {
|
|
||||||
if metadata.is_dir() {
|
|
||||||
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 hash
|
|
||||||
if path.contains('\'')
|
|
||||||
|| path.contains('"')
|
|
||||||
|| path.contains(' ')
|
|
||||||
|| path.contains('#')
|
|
||||||
{
|
|
||||||
path = format!("`{path}`");
|
|
||||||
}
|
|
||||||
|
|
||||||
Some((span, path))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
Vec::new()
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::completions::{Completer, CompletionOptions};
|
use crate::completions::{completion_common::complete_item, Completer, CompletionOptions};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{EngineState, StateWorkingSet},
|
engine::{EngineState, StateWorkingSet},
|
||||||
levenshtein_distance, Span,
|
levenshtein_distance, Span,
|
||||||
@ -32,21 +32,25 @@ impl Completer for FileCompletion {
|
|||||||
_: usize,
|
_: usize,
|
||||||
options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
) -> Vec<Suggestion> {
|
) -> Vec<Suggestion> {
|
||||||
let cwd = self.engine_state.current_work_dir();
|
|
||||||
let prefix = String::from_utf8_lossy(&prefix).to_string();
|
let prefix = String::from_utf8_lossy(&prefix).to_string();
|
||||||
let output: Vec<_> = file_path_completion(span, &prefix, &cwd, options)
|
let output: Vec<_> = file_path_completion(
|
||||||
.into_iter()
|
span,
|
||||||
.map(move |x| Suggestion {
|
&prefix,
|
||||||
value: x.1,
|
&self.engine_state.current_work_dir(),
|
||||||
description: None,
|
options,
|
||||||
extra: None,
|
)
|
||||||
span: reedline::Span {
|
.into_iter()
|
||||||
start: x.0.start - offset,
|
.map(move |x| Suggestion {
|
||||||
end: x.0.end - offset,
|
value: x.1,
|
||||||
},
|
description: None,
|
||||||
append_whitespace: false,
|
extra: None,
|
||||||
})
|
span: reedline::Span {
|
||||||
.collect();
|
start: x.0.start - offset,
|
||||||
|
end: x.0.end - offset,
|
||||||
|
},
|
||||||
|
append_whitespace: false,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
@ -122,64 +126,7 @@ pub fn file_path_completion(
|
|||||||
cwd: &str,
|
cwd: &str,
|
||||||
options: &CompletionOptions,
|
options: &CompletionOptions,
|
||||||
) -> Vec<(nu_protocol::Span, String)> {
|
) -> Vec<(nu_protocol::Span, String)> {
|
||||||
let original_input = partial;
|
complete_item(false, span, partial, cwd, options)
|
||||||
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 {
|
pub fn matches(partial: &str, from: &str, options: &CompletionOptions) -> bool {
|
||||||
@ -192,23 +139,3 @@ pub fn matches(partial: &str, from: &str, options: &CompletionOptions) -> bool {
|
|||||||
|
|
||||||
options.match_algorithm.matches_str(from, partial)
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
mod base;
|
mod base;
|
||||||
mod command_completions;
|
mod command_completions;
|
||||||
mod completer;
|
mod completer;
|
||||||
|
mod completion_common;
|
||||||
mod completion_options;
|
mod completion_options;
|
||||||
mod custom_completions;
|
mod custom_completions;
|
||||||
mod directory_completions;
|
mod directory_completions;
|
||||||
@ -16,8 +17,6 @@ pub use completion_options::{CompletionOptions, MatchAlgorithm, SortBy};
|
|||||||
pub use custom_completions::CustomCompletion;
|
pub use custom_completions::CustomCompletion;
|
||||||
pub use directory_completions::DirectoryCompletion;
|
pub use directory_completions::DirectoryCompletion;
|
||||||
pub use dotnu_completions::DotNuCompletion;
|
pub use dotnu_completions::DotNuCompletion;
|
||||||
pub use file_completions::{
|
pub use file_completions::{file_path_completion, matches, partial_from, FileCompletion};
|
||||||
file_path_completion, matches, partial_from, prepend_base_dir, FileCompletion,
|
|
||||||
};
|
|
||||||
pub use flag_completions::FlagCompletion;
|
pub use flag_completions::FlagCompletion;
|
||||||
pub use variable_completions::VariableCompletion;
|
pub use variable_completions::VariableCompletion;
|
||||||
|
@ -5,7 +5,10 @@ use nu_parser::parse;
|
|||||||
use nu_protocol::engine::StateWorkingSet;
|
use nu_protocol::engine::StateWorkingSet;
|
||||||
use reedline::{Completer, Suggestion};
|
use reedline::{Completer, Suggestion};
|
||||||
use rstest::{fixture, rstest};
|
use rstest::{fixture, rstest};
|
||||||
use support::{completions_helpers::new_quote_engine, file, folder, match_suggestions, new_engine};
|
use support::{
|
||||||
|
completions_helpers::{new_partial_engine, new_quote_engine},
|
||||||
|
file, folder, match_suggestions, new_engine,
|
||||||
|
};
|
||||||
|
|
||||||
#[fixture]
|
#[fixture]
|
||||||
fn completer() -> NuCompleter {
|
fn completer() -> NuCompleter {
|
||||||
@ -201,6 +204,87 @@ fn file_completions() {
|
|||||||
match_suggestions(expected_paths, suggestions);
|
match_suggestions(expected_paths, suggestions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn partial_completions() {
|
||||||
|
// Create a new engine
|
||||||
|
let (dir, _, engine, stack) = new_partial_engine();
|
||||||
|
|
||||||
|
// Instantiate a new completer
|
||||||
|
let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
|
||||||
|
|
||||||
|
// Test completions for a folder's name
|
||||||
|
let target_dir = format!("cd {}", file(dir.join("pa")));
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
// Create the expected values
|
||||||
|
let expected_paths: Vec<String> = vec![
|
||||||
|
folder(dir.join("partial_a")),
|
||||||
|
folder(dir.join("partial_b")),
|
||||||
|
folder(dir.join("partial_c")),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Match the results
|
||||||
|
match_suggestions(expected_paths, suggestions);
|
||||||
|
|
||||||
|
// Test completions for the files whose name begin with "h"
|
||||||
|
// and are present under directories whose names begin with "pa"
|
||||||
|
let dir_str = file(dir.join("pa").join("h"));
|
||||||
|
let target_dir = format!("cp {dir_str}");
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
// Create the expected values
|
||||||
|
let expected_paths: Vec<String> = vec![
|
||||||
|
file(dir.join("partial_a").join("hello")),
|
||||||
|
file(dir.join("partial_a").join("hola")),
|
||||||
|
file(dir.join("partial_b").join("hello_b")),
|
||||||
|
file(dir.join("partial_b").join("hi_b")),
|
||||||
|
file(dir.join("partial_c").join("hello_c")),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Match the results
|
||||||
|
match_suggestions(expected_paths, suggestions);
|
||||||
|
|
||||||
|
// Test completion for all files under directories whose names begin with "pa"
|
||||||
|
let dir_str = folder(dir.join("pa"));
|
||||||
|
let target_dir = format!("ls {dir_str}");
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
// Create the expected values
|
||||||
|
let expected_paths: Vec<String> = vec![
|
||||||
|
file(dir.join("partial_a").join("anotherfile")),
|
||||||
|
file(dir.join("partial_a").join("hello")),
|
||||||
|
file(dir.join("partial_a").join("hola")),
|
||||||
|
file(dir.join("partial_b").join("hello_b")),
|
||||||
|
file(dir.join("partial_b").join("hi_b")),
|
||||||
|
file(dir.join("partial_c").join("hello_c")),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Match the results
|
||||||
|
match_suggestions(expected_paths, suggestions);
|
||||||
|
|
||||||
|
// Test completion for a single file
|
||||||
|
let dir_str = file(dir.join("fi").join("so"));
|
||||||
|
let target_dir = format!("rm {dir_str}");
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
// Create the expected values
|
||||||
|
let expected_paths: Vec<String> = vec![file(dir.join("final_partial").join("somefile"))];
|
||||||
|
|
||||||
|
// Match the results
|
||||||
|
match_suggestions(expected_paths, suggestions);
|
||||||
|
|
||||||
|
// Test completion where there is a sneaky `..` in the path
|
||||||
|
let dir_str = file(dir.join("par").join("..").join("fi").join("so"));
|
||||||
|
let target_dir = format!("rm {dir_str}");
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
// Create the expected values
|
||||||
|
let expected_paths: Vec<String> = vec![file(dir.join("final_partial").join("somefile"))];
|
||||||
|
|
||||||
|
// Match the results
|
||||||
|
match_suggestions(expected_paths, suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn command_ls_with_filecompletion() {
|
fn command_ls_with_filecompletion() {
|
||||||
let (_, _, engine, stack) = new_engine();
|
let (_, _, engine, stack) = new_engine();
|
||||||
|
@ -109,6 +109,42 @@ pub fn new_quote_engine() -> (PathBuf, String, EngineState, Stack) {
|
|||||||
(dir, dir_str, engine_state, stack)
|
(dir, dir_str, engine_state, stack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_partial_engine() -> (PathBuf, String, EngineState, Stack) {
|
||||||
|
// Target folder inside assets
|
||||||
|
let dir = fs::fixtures().join("partial_completions");
|
||||||
|
let mut dir_str = dir
|
||||||
|
.clone()
|
||||||
|
.into_os_string()
|
||||||
|
.into_string()
|
||||||
|
.unwrap_or_default();
|
||||||
|
dir_str.push(SEP);
|
||||||
|
|
||||||
|
// Create a new engine with default context
|
||||||
|
let mut engine_state = create_default_context();
|
||||||
|
|
||||||
|
// New stack
|
||||||
|
let mut stack = Stack::new();
|
||||||
|
|
||||||
|
// Add pwd as env var
|
||||||
|
stack.add_env_var(
|
||||||
|
"PWD".to_string(),
|
||||||
|
Value::string(dir_str.clone(), nu_protocol::Span::new(0, dir_str.len())),
|
||||||
|
);
|
||||||
|
stack.add_env_var(
|
||||||
|
"TEST".to_string(),
|
||||||
|
Value::string(
|
||||||
|
"NUSHELL".to_string(),
|
||||||
|
nu_protocol::Span::new(0, dir_str.len()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Merge environment into the permanent state
|
||||||
|
let merge_result = engine_state.merge_env(&mut stack, &dir);
|
||||||
|
assert!(merge_result.is_ok());
|
||||||
|
|
||||||
|
(dir, dir_str, engine_state, stack)
|
||||||
|
}
|
||||||
|
|
||||||
// match a list of suggestions with the expected values
|
// match a list of suggestions with the expected values
|
||||||
pub fn match_suggestions(expected: Vec<String>, suggestions: Vec<Suggestion>) {
|
pub fn match_suggestions(expected: Vec<String>, suggestions: Vec<Suggestion>) {
|
||||||
let expected_len = expected.len();
|
let expected_len = expected.len();
|
||||||
|
0
tests/fixtures/partial_completions/final_partial/somefile
vendored
Normal file
0
tests/fixtures/partial_completions/final_partial/somefile
vendored
Normal file
0
tests/fixtures/partial_completions/partial_a/anotherfile
vendored
Normal file
0
tests/fixtures/partial_completions/partial_a/anotherfile
vendored
Normal file
0
tests/fixtures/partial_completions/partial_a/hello
vendored
Normal file
0
tests/fixtures/partial_completions/partial_a/hello
vendored
Normal file
0
tests/fixtures/partial_completions/partial_a/hola
vendored
Normal file
0
tests/fixtures/partial_completions/partial_a/hola
vendored
Normal file
0
tests/fixtures/partial_completions/partial_b/hello_b
vendored
Normal file
0
tests/fixtures/partial_completions/partial_b/hello_b
vendored
Normal file
0
tests/fixtures/partial_completions/partial_b/hi_b
vendored
Normal file
0
tests/fixtures/partial_completions/partial_b/hi_b
vendored
Normal file
0
tests/fixtures/partial_completions/partial_c/hello_c
vendored
Normal file
0
tests/fixtures/partial_completions/partial_c/hello_c
vendored
Normal file
Loading…
Reference in New Issue
Block a user