forked from extern/nushell
<!-- 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 #xxxx - fixes #xxxx you can also mention related issues, PRs or discussions! --> Refer to https://github.com/nushell/nushell/pull/10600#issuecomment-1762863791 # 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. --> A path is escaped when it can be entirely parsed as a floating point number. This includes `nan`, `inf` and their negative counterparts since nu also supports them. # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> Paths with numbers that cannot be ambiguous are no longer surrounded by backticks. # 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 > ``` --> # 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. -->
161 lines
5.2 KiB
Rust
161 lines
5.2 KiB
Rust
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),
|
|
// referencing a single local file
|
|
Local(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(),
|
|
},
|
|
Self::Local(base) => Path::new(".")
|
|
.join(pathdiff::diff_paths(p, base).unwrap_or(p.to_path_buf()))
|
|
.to_string_lossy()
|
|
.into_owned(),
|
|
};
|
|
|
|
if p.is_dir() {
|
|
ret.push(SEP);
|
|
}
|
|
ret
|
|
}
|
|
}
|
|
|
|
fn surround_remove(partial: &str) -> String {
|
|
for c in ['`', '"', '\''] {
|
|
if partial.starts_with(c) {
|
|
let ret = partial.strip_prefix(c).unwrap_or(partial);
|
|
return match ret.split(c).collect::<Vec<_>>()[..] {
|
|
[inside] => inside.to_string(),
|
|
[inside, outside] if inside.ends_with(is_separator) => format!("{inside}{outside}"),
|
|
_ => ret.to_string(),
|
|
};
|
|
}
|
|
}
|
|
partial.to_string()
|
|
}
|
|
|
|
pub fn complete_item(
|
|
want_directory: bool,
|
|
span: nu_protocol::Span,
|
|
partial: &str,
|
|
cwd: &str,
|
|
options: &CompletionOptions,
|
|
) -> Vec<(nu_protocol::Span, String)> {
|
|
let partial = surround_remove(partial);
|
|
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)
|
|
}
|
|
Some(Component::CurDir) => {
|
|
components.next();
|
|
original_cwd = match components.peek().cloned() {
|
|
Some(Component::Normal(_)) | None => OriginalCwd::Local(cwd_pathbuf.clone()),
|
|
_ => OriginalCwd::Some(cwd_pathbuf.clone()),
|
|
};
|
|
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(['\'', '"', ' ', '#', '(', ')']);
|
|
let dirname_contaminated = dir && path.contains(['\'', '"', ' ', '#']);
|
|
if filename_contaminated || dirname_contaminated || path.parse::<f64>().is_ok() {
|
|
format!("`{path}`")
|
|
} else {
|
|
path
|
|
}
|
|
}
|