use super::{completion_options::NuMatcher, MatchAlgorithm}; use crate::completions::CompletionOptions; 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}, Span, }; use nu_utils::get_ls_colors; use nu_utils::IgnoreCaseExt; use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP}; #[derive(Clone, Default)] pub struct PathBuiltFromString { cwd: PathBuf, parts: Vec, isdir: bool, } /// Recursively goes through paths that match a given `partial`. /// built: State struct for a valid matching path built so far. /// /// `isdir`: whether the current partial path has a trailing slash. /// Parsing a path string into a pathbuf loses that bit of information. /// /// want_directory: Whether we want only directories as completion matches. /// Some commands like `cd` can only be run on directories whereas others /// like `ls` can be run on regular files as well. fn complete_rec( partial: &[&str], built_paths: &[PathBuiltFromString], options: &CompletionOptions, want_directory: bool, isdir: bool, ) -> Vec { if let Some((&base, rest)) = partial.split_first() { if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) { let built_paths: Vec<_> = built_paths .iter() .map(|built| { let mut built = built.clone(); built.parts.push(base.to_string()); built.isdir = true; built }) .collect(); return complete_rec(rest, &built_paths, options, want_directory, isdir); } } let prefix = partial.first().unwrap_or(&""); let mut matcher = NuMatcher::new(prefix, options.clone()); for built in built_paths { let mut path = built.cwd.clone(); for part in &built.parts { path.push(part); } let Ok(result) = path.read_dir() else { continue; }; for entry in result.filter_map(|e| e.ok()) { let entry_name = entry.file_name().to_string_lossy().into_owned(); let entry_isdir = entry.path().is_dir(); let mut built = built.clone(); built.parts.push(entry_name.clone()); built.isdir = entry_isdir; if !want_directory || entry_isdir { matcher.add(entry_name.clone(), (entry_name, built)); } } } let mut completions = vec![]; for (entry_name, built) in matcher.results() { match partial.split_first() { Some((base, rest)) => { // We use `isdir` to confirm that the current component has // at least one next component or a slash. // Serves as confirmation to ignore longer completions for // components in between. if !rest.is_empty() || isdir { completions.extend(complete_rec( rest, &[built], options, want_directory, isdir, )); } else { completions.push(built); } // For https://github.com/nushell/nushell/issues/13204 if isdir && options.match_algorithm == MatchAlgorithm::Prefix { let exact_match = if options.case_sensitive { entry_name.eq(base) } else { entry_name.to_folded_case().eq(&base.to_folded_case()) }; if exact_match { break; } } } None => { completions.push(built); } } } completions } #[derive(Debug)] enum OriginalCwd { None, Home, Prefix(String), } impl OriginalCwd { fn apply(&self, mut p: PathBuiltFromString, path_separator: char) -> String { match self { Self::None => {} Self::Home => p.parts.insert(0, "~".to_string()), Self::Prefix(s) => p.parts.insert(0, s.clone()), }; let mut ret = p.parts.join(&path_separator.to_string()); if p.isdir { ret.push(path_separator); } 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::>()[..] { [inside] => inside.to_string(), [inside, outside] if inside.ends_with(is_separator) => format!("{inside}{outside}"), _ => ret.to_string(), }; } } partial.to_string() } pub struct FileSuggestion { pub span: nu_protocol::Span, pub path: String, pub style: Option