mirror of
https://github.com/nushell/nushell.git
synced 2024-11-22 08:23:24 +01:00
Expand multiple dots in path in completions (#13725)
# Description
This is my first PR, and I'm looking for feedback to help me improve!
This PR fixes #13380 by expanding the path prior to parsing it.
Also I've removed some unused code in
[completion_common.rs](84e92bb02c/crates/nu-cli/src/completions/completion_common.rs
)
# User-Facing Changes
Auto-completion for "cd .../" now works by expanding to "cd ../../".
# Tests + Formatting
Formatted and added 2 tests for triple dots in the middle of a path and
at the end.
Also added a test for the expand_ndots() function.
This commit is contained in:
parent
aff974552a
commit
6600b3edfb
@ -1,3 +1,4 @@
|
|||||||
|
use super::MatchAlgorithm;
|
||||||
use crate::{
|
use crate::{
|
||||||
completions::{matches, CompletionOptions},
|
completions::{matches, CompletionOptions},
|
||||||
SemanticSuggestion,
|
SemanticSuggestion,
|
||||||
@ -5,6 +6,7 @@ use crate::{
|
|||||||
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
||||||
use nu_ansi_term::Style;
|
use nu_ansi_term::Style;
|
||||||
use nu_engine::env_to_string;
|
use nu_engine::env_to_string;
|
||||||
|
use nu_path::dots::expand_ndots;
|
||||||
use nu_path::{expand_to_real_path, home_dir};
|
use nu_path::{expand_to_real_path, home_dir};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{EngineState, Stack, StateWorkingSet},
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
@ -13,8 +15,6 @@ use nu_protocol::{
|
|||||||
use nu_utils::get_ls_colors;
|
use nu_utils::get_ls_colors;
|
||||||
use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP};
|
use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP};
|
||||||
|
|
||||||
use super::MatchAlgorithm;
|
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct PathBuiltFromString {
|
pub struct PathBuiltFromString {
|
||||||
parts: Vec<String>,
|
parts: Vec<String>,
|
||||||
@ -41,7 +41,7 @@ pub fn complete_rec(
|
|||||||
let mut completions = vec![];
|
let mut completions = vec![];
|
||||||
|
|
||||||
if let Some((&base, rest)) = partial.split_first() {
|
if let Some((&base, rest)) = partial.split_first() {
|
||||||
if (base == "." || base == "..") && (isdir || !rest.is_empty()) {
|
if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) {
|
||||||
let mut built = built.clone();
|
let mut built = built.clone();
|
||||||
built.parts.push(base.to_string());
|
built.parts.push(base.to_string());
|
||||||
built.isdir = true;
|
built.isdir = true;
|
||||||
@ -156,16 +156,25 @@ pub fn complete_item(
|
|||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &Stack,
|
stack: &Stack,
|
||||||
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
|
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
|
||||||
let partial = surround_remove(partial);
|
let cleaned_partial = surround_remove(partial);
|
||||||
let isdir = partial.ends_with(is_separator);
|
let isdir = cleaned_partial.ends_with(is_separator);
|
||||||
|
let expanded_partial = expand_ndots(Path::new(&cleaned_partial));
|
||||||
|
let should_collapse_dots = expanded_partial != Path::new(&cleaned_partial);
|
||||||
|
let mut partial = expanded_partial.to_string_lossy().to_string();
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let path_separator = SEP;
|
let path_separator = SEP;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let path_separator = partial
|
let path_separator = cleaned_partial
|
||||||
.chars()
|
.chars()
|
||||||
.rfind(|c: &char| is_separator(*c))
|
.rfind(|c: &char| is_separator(*c))
|
||||||
.unwrap_or(SEP);
|
.unwrap_or(SEP);
|
||||||
|
|
||||||
|
// Handle the trailing dot case
|
||||||
|
if cleaned_partial.ends_with(&format!("{path_separator}.")) {
|
||||||
|
partial.push_str(&format!("{path_separator}."));
|
||||||
|
}
|
||||||
|
|
||||||
let cwd_pathbuf = Path::new(cwd).to_path_buf();
|
let cwd_pathbuf = Path::new(cwd).to_path_buf();
|
||||||
let ls_colors = (engine_state.config.completions.use_ls_colors
|
let ls_colors = (engine_state.config.completions.use_ls_colors
|
||||||
&& engine_state.config.use_ansi_coloring)
|
&& engine_state.config.use_ansi_coloring)
|
||||||
@ -185,16 +194,11 @@ pub fn complete_item(
|
|||||||
match components.peek().cloned() {
|
match components.peek().cloned() {
|
||||||
Some(c @ Component::Prefix(..)) => {
|
Some(c @ Component::Prefix(..)) => {
|
||||||
// windows only by definition
|
// windows only by definition
|
||||||
components.next();
|
|
||||||
if let Some(Component::RootDir) = components.peek().cloned() {
|
|
||||||
components.next();
|
|
||||||
};
|
|
||||||
cwd = [c, Component::RootDir].iter().collect();
|
cwd = [c, Component::RootDir].iter().collect();
|
||||||
prefix_len = c.as_os_str().len();
|
prefix_len = c.as_os_str().len();
|
||||||
original_cwd = OriginalCwd::Prefix(c.as_os_str().to_string_lossy().into_owned());
|
original_cwd = OriginalCwd::Prefix(c.as_os_str().to_string_lossy().into_owned());
|
||||||
}
|
}
|
||||||
Some(c @ Component::RootDir) => {
|
Some(c @ Component::RootDir) => {
|
||||||
components.next();
|
|
||||||
// This is kind of a hack. When joining an empty string with the rest,
|
// This is kind of a hack. When joining an empty string with the rest,
|
||||||
// we add the slash automagically
|
// we add the slash automagically
|
||||||
cwd = PathBuf::from(c.as_os_str());
|
cwd = PathBuf::from(c.as_os_str());
|
||||||
@ -202,7 +206,6 @@ pub fn complete_item(
|
|||||||
original_cwd = OriginalCwd::Prefix(String::new());
|
original_cwd = OriginalCwd::Prefix(String::new());
|
||||||
}
|
}
|
||||||
Some(Component::Normal(home)) if home.to_string_lossy() == "~" => {
|
Some(Component::Normal(home)) if home.to_string_lossy() == "~" => {
|
||||||
components.next();
|
|
||||||
cwd = home_dir().map(Into::into).unwrap_or(cwd_pathbuf);
|
cwd = home_dir().map(Into::into).unwrap_or(cwd_pathbuf);
|
||||||
prefix_len = 1;
|
prefix_len = 1;
|
||||||
original_cwd = OriginalCwd::Home;
|
original_cwd = OriginalCwd::Home;
|
||||||
@ -227,7 +230,10 @@ pub fn complete_item(
|
|||||||
isdir,
|
isdir,
|
||||||
)
|
)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|p| {
|
.map(|mut p| {
|
||||||
|
if should_collapse_dots {
|
||||||
|
p = collapse_ndots(p);
|
||||||
|
}
|
||||||
let path = original_cwd.apply(p, path_separator);
|
let path = original_cwd.apply(p, path_separator);
|
||||||
let style = ls_colors.as_ref().map(|lsc| {
|
let style = ls_colors.as_ref().map(|lsc| {
|
||||||
lsc.style_for_path_with_metadata(
|
lsc.style_for_path_with_metadata(
|
||||||
@ -340,3 +346,37 @@ pub fn sort_completions<T>(
|
|||||||
|
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collapse multiple ".." components into n-dots.
|
||||||
|
///
|
||||||
|
/// It performs the reverse operation of `expand_ndots`, collapsing sequences of ".." into n-dots,
|
||||||
|
/// such as "..." and "....".
|
||||||
|
///
|
||||||
|
/// The resulting path will use platform-specific path separators, regardless of what path separators were used in the input.
|
||||||
|
fn collapse_ndots(path: PathBuiltFromString) -> PathBuiltFromString {
|
||||||
|
let mut result = PathBuiltFromString {
|
||||||
|
parts: Vec::with_capacity(path.parts.len()),
|
||||||
|
isdir: path.isdir,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut dot_count = 0;
|
||||||
|
|
||||||
|
for part in path.parts {
|
||||||
|
if part == ".." {
|
||||||
|
dot_count += 1;
|
||||||
|
} else {
|
||||||
|
if dot_count > 0 {
|
||||||
|
result.parts.push(".".repeat(dot_count + 1));
|
||||||
|
dot_count = 0;
|
||||||
|
}
|
||||||
|
result.parts.push(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining dots
|
||||||
|
if dot_count > 0 {
|
||||||
|
result.parts.push(".".repeat(dot_count + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
@ -339,7 +339,7 @@ fn file_completions() {
|
|||||||
match_suggestions(&expected_paths, &suggestions);
|
match_suggestions(&expected_paths, &suggestions);
|
||||||
|
|
||||||
// Test completions for hidden files
|
// Test completions for hidden files
|
||||||
let target_dir = format!("ls {}{MAIN_SEPARATOR}.", folder(dir.join(".hidden_folder")));
|
let target_dir = format!("ls {}", file(dir.join(".hidden_folder").join(".")));
|
||||||
let suggestions = completer.complete(&target_dir, target_dir.len());
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
let expected_paths: Vec<String> =
|
let expected_paths: Vec<String> =
|
||||||
@ -564,6 +564,58 @@ fn partial_completions() {
|
|||||||
match_suggestions(&expected_paths, &suggestions);
|
match_suggestions(&expected_paths, &suggestions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn partial_completion_with_dot_expansions() {
|
||||||
|
let (dir, _, engine, stack) = new_partial_engine();
|
||||||
|
|
||||||
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
|
||||||
|
let dir_str = file(
|
||||||
|
dir.join("par")
|
||||||
|
.join("...")
|
||||||
|
.join("par")
|
||||||
|
.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("partial")
|
||||||
|
.join("...")
|
||||||
|
.join("partial_completions")
|
||||||
|
.join("final_partial")
|
||||||
|
.join("somefile"),
|
||||||
|
),
|
||||||
|
file(
|
||||||
|
dir.join("partial-a")
|
||||||
|
.join("...")
|
||||||
|
.join("partial_completions")
|
||||||
|
.join("final_partial")
|
||||||
|
.join("somefile"),
|
||||||
|
),
|
||||||
|
file(
|
||||||
|
dir.join("partial-b")
|
||||||
|
.join("...")
|
||||||
|
.join("partial_completions")
|
||||||
|
.join("final_partial")
|
||||||
|
.join("somefile"),
|
||||||
|
),
|
||||||
|
file(
|
||||||
|
dir.join("partial-c")
|
||||||
|
.join("...")
|
||||||
|
.join("partial_completions")
|
||||||
|
.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();
|
||||||
@ -953,6 +1005,192 @@ fn folder_with_directorycompletions() {
|
|||||||
match_suggestions(&expected_paths, &suggestions);
|
match_suggestions(&expected_paths, &suggestions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn folder_with_directorycompletions_with_dots() {
|
||||||
|
// Create a new engine
|
||||||
|
let (dir, _, engine, stack) = new_engine();
|
||||||
|
let dir_str = dir
|
||||||
|
.join("directory_completion")
|
||||||
|
.join("folder_inside_folder")
|
||||||
|
.into_os_string()
|
||||||
|
.into_string()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Instantiate a new completer
|
||||||
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
|
||||||
|
// Test completions for the current folder
|
||||||
|
let target_dir = format!("cd {dir_str}{MAIN_SEPARATOR}..{MAIN_SEPARATOR}");
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
// Create the expected values
|
||||||
|
let expected_paths: Vec<String> = vec![folder(
|
||||||
|
dir.join("directory_completion")
|
||||||
|
.join("folder_inside_folder")
|
||||||
|
.join("..")
|
||||||
|
.join("folder_inside_folder"),
|
||||||
|
)];
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let target_dir = format!("cd {dir_str}/../");
|
||||||
|
let slash_suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
let expected_slash_paths: Vec<String> = expected_paths
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.replace('\\', "/"))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match_suggestions(&expected_slash_paths, &slash_suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match the results
|
||||||
|
match_suggestions(&expected_paths, &suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn folder_with_directorycompletions_with_three_trailing_dots() {
|
||||||
|
// Create a new engine
|
||||||
|
let (dir, _, engine, stack) = new_engine();
|
||||||
|
let dir_str = dir
|
||||||
|
.join("directory_completion")
|
||||||
|
.join("folder_inside_folder")
|
||||||
|
.into_os_string()
|
||||||
|
.into_string()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Instantiate a new completer
|
||||||
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
|
||||||
|
// Test completions for the current folder
|
||||||
|
let target_dir = format!("cd {dir_str}{MAIN_SEPARATOR}...{MAIN_SEPARATOR}");
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
// Create the expected values
|
||||||
|
let expected_paths: Vec<String> = vec![
|
||||||
|
folder(
|
||||||
|
dir.join("directory_completion")
|
||||||
|
.join("folder_inside_folder")
|
||||||
|
.join("...")
|
||||||
|
.join("another"),
|
||||||
|
),
|
||||||
|
folder(
|
||||||
|
dir.join("directory_completion")
|
||||||
|
.join("folder_inside_folder")
|
||||||
|
.join("...")
|
||||||
|
.join("directory_completion"),
|
||||||
|
),
|
||||||
|
folder(
|
||||||
|
dir.join("directory_completion")
|
||||||
|
.join("folder_inside_folder")
|
||||||
|
.join("...")
|
||||||
|
.join("test_a"),
|
||||||
|
),
|
||||||
|
folder(
|
||||||
|
dir.join("directory_completion")
|
||||||
|
.join("folder_inside_folder")
|
||||||
|
.join("...")
|
||||||
|
.join("test_b"),
|
||||||
|
),
|
||||||
|
folder(
|
||||||
|
dir.join("directory_completion")
|
||||||
|
.join("folder_inside_folder")
|
||||||
|
.join("...")
|
||||||
|
.join(".hidden_folder"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let target_dir = format!("cd {dir_str}/.../");
|
||||||
|
let slash_suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
let expected_slash_paths: Vec<String> = expected_paths
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.replace('\\', "/"))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match_suggestions(&expected_slash_paths, &slash_suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match the results
|
||||||
|
match_suggestions(&expected_paths, &suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn folder_with_directorycompletions_do_not_collapse_dots() {
|
||||||
|
// Create a new engine
|
||||||
|
let (dir, _, engine, stack) = new_engine();
|
||||||
|
let dir_str = dir
|
||||||
|
.join("directory_completion")
|
||||||
|
.join("folder_inside_folder")
|
||||||
|
.into_os_string()
|
||||||
|
.into_string()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Instantiate a new completer
|
||||||
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
|
||||||
|
// Test completions for the current folder
|
||||||
|
let target_dir = format!("cd {dir_str}{MAIN_SEPARATOR}..{MAIN_SEPARATOR}..{MAIN_SEPARATOR}");
|
||||||
|
let suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
// Create the expected values
|
||||||
|
let expected_paths: Vec<String> = vec![
|
||||||
|
folder(
|
||||||
|
dir.join("directory_completion")
|
||||||
|
.join("folder_inside_folder")
|
||||||
|
.join("..")
|
||||||
|
.join("..")
|
||||||
|
.join("another"),
|
||||||
|
),
|
||||||
|
folder(
|
||||||
|
dir.join("directory_completion")
|
||||||
|
.join("folder_inside_folder")
|
||||||
|
.join("..")
|
||||||
|
.join("..")
|
||||||
|
.join("directory_completion"),
|
||||||
|
),
|
||||||
|
folder(
|
||||||
|
dir.join("directory_completion")
|
||||||
|
.join("folder_inside_folder")
|
||||||
|
.join("..")
|
||||||
|
.join("..")
|
||||||
|
.join("test_a"),
|
||||||
|
),
|
||||||
|
folder(
|
||||||
|
dir.join("directory_completion")
|
||||||
|
.join("folder_inside_folder")
|
||||||
|
.join("..")
|
||||||
|
.join("..")
|
||||||
|
.join("test_b"),
|
||||||
|
),
|
||||||
|
folder(
|
||||||
|
dir.join("directory_completion")
|
||||||
|
.join("folder_inside_folder")
|
||||||
|
.join("..")
|
||||||
|
.join("..")
|
||||||
|
.join(".hidden_folder"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let target_dir = format!("cd {dir_str}/../../");
|
||||||
|
let slash_suggestions = completer.complete(&target_dir, target_dir.len());
|
||||||
|
|
||||||
|
let expected_slash_paths: Vec<String> = expected_paths
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.replace('\\', "/"))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match_suggestions(&expected_slash_paths, &slash_suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match the results
|
||||||
|
match_suggestions(&expected_paths, &suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn variables_completions() {
|
fn variables_completions() {
|
||||||
// Create a new engine
|
// Create a new engine
|
||||||
|
@ -152,6 +152,17 @@ mod test_expand_ndots {
|
|||||||
};
|
};
|
||||||
assert_path_eq!(expand_ndots(path), expected);
|
assert_path_eq!(expand_ndots(path), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trailing_dots() {
|
||||||
|
let path = Path::new("/foo/bar/..");
|
||||||
|
let expected = if cfg!(windows) {
|
||||||
|
r"\foo\bar\.."
|
||||||
|
} else {
|
||||||
|
"/foo/bar/.."
|
||||||
|
};
|
||||||
|
assert_path_eq!(expand_ndots(path), expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
0
tests/fixtures/completions/directory_completion/folder_inside_folder/myfile
vendored
Normal file
0
tests/fixtures/completions/directory_completion/folder_inside_folder/myfile
vendored
Normal file
Loading…
Reference in New Issue
Block a user