forked from extern/nushell
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:
@ -1,3 +1,4 @@
|
||||
use super::MatchAlgorithm;
|
||||
use crate::{
|
||||
completions::{matches, CompletionOptions},
|
||||
SemanticSuggestion,
|
||||
@ -5,6 +6,7 @@ use crate::{
|
||||
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
||||
use nu_ansi_term::Style;
|
||||
use nu_engine::env_to_string;
|
||||
use nu_path::dots::expand_ndots;
|
||||
use nu_path::{expand_to_real_path, home_dir};
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
@ -13,8 +15,6 @@ use nu_protocol::{
|
||||
use nu_utils::get_ls_colors;
|
||||
use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP};
|
||||
|
||||
use super::MatchAlgorithm;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct PathBuiltFromString {
|
||||
parts: Vec<String>,
|
||||
@ -41,7 +41,7 @@ pub fn complete_rec(
|
||||
let mut completions = vec![];
|
||||
|
||||
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();
|
||||
built.parts.push(base.to_string());
|
||||
built.isdir = true;
|
||||
@ -156,16 +156,25 @@ pub fn complete_item(
|
||||
engine_state: &EngineState,
|
||||
stack: &Stack,
|
||||
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
|
||||
let partial = surround_remove(partial);
|
||||
let isdir = partial.ends_with(is_separator);
|
||||
let cleaned_partial = surround_remove(partial);
|
||||
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)]
|
||||
let path_separator = SEP;
|
||||
#[cfg(windows)]
|
||||
let path_separator = partial
|
||||
let path_separator = cleaned_partial
|
||||
.chars()
|
||||
.rfind(|c: &char| is_separator(*c))
|
||||
.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 ls_colors = (engine_state.config.completions.use_ls_colors
|
||||
&& engine_state.config.use_ansi_coloring)
|
||||
@ -185,16 +194,11 @@ pub fn complete_item(
|
||||
match components.peek().cloned() {
|
||||
Some(c @ Component::Prefix(..)) => {
|
||||
// windows only by definition
|
||||
components.next();
|
||||
if let Some(Component::RootDir) = components.peek().cloned() {
|
||||
components.next();
|
||||
};
|
||||
cwd = [c, Component::RootDir].iter().collect();
|
||||
prefix_len = c.as_os_str().len();
|
||||
original_cwd = OriginalCwd::Prefix(c.as_os_str().to_string_lossy().into_owned());
|
||||
}
|
||||
Some(c @ Component::RootDir) => {
|
||||
components.next();
|
||||
// This is kind of a hack. When joining an empty string with the rest,
|
||||
// we add the slash automagically
|
||||
cwd = PathBuf::from(c.as_os_str());
|
||||
@ -202,7 +206,6 @@ pub fn complete_item(
|
||||
original_cwd = OriginalCwd::Prefix(String::new());
|
||||
}
|
||||
Some(Component::Normal(home)) if home.to_string_lossy() == "~" => {
|
||||
components.next();
|
||||
cwd = home_dir().map(Into::into).unwrap_or(cwd_pathbuf);
|
||||
prefix_len = 1;
|
||||
original_cwd = OriginalCwd::Home;
|
||||
@ -227,7 +230,10 @@ pub fn complete_item(
|
||||
isdir,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
.map(|mut p| {
|
||||
if should_collapse_dots {
|
||||
p = collapse_ndots(p);
|
||||
}
|
||||
let path = original_cwd.apply(p, path_separator);
|
||||
let style = ls_colors.as_ref().map(|lsc| {
|
||||
lsc.style_for_path_with_metadata(
|
||||
@ -340,3 +346,37 @@ pub fn sort_completions<T>(
|
||||
|
||||
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
|
||||
}
|
||||
|
Reference in New Issue
Block a user