use crate::{ completions::{matches, CompletionOptions}, SemanticSuggestion, }; use nu_ansi_term::Style; use nu_engine::env_to_string; use nu_path::{expand_to_real_path, home_dir}; use nu_protocol::{ engine::{EngineState, Stack, StateWorkingSet}, levenshtein_distance, Span, }; use nu_utils::get_ls_colors; use std::path::{ is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR, }; use super::{MatchAlgorithm, SortBy}; #[derive(Clone, Default)] pub struct PathBuiltFromString { 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. pub fn complete_rec( partial: &[&str], built: &PathBuiltFromString, cwd: &Path, options: &CompletionOptions, want_directory: bool, isdir: bool, ) -> Vec { let mut completions = vec![]; if let Some((&base, rest)) = partial.split_first() { if (base == "." || base == "..") && (isdir || !rest.is_empty()) { let mut built = built.clone(); built.parts.push(base.to_string()); built.isdir = true; return complete_rec(rest, &built, cwd, options, want_directory, isdir); } } let mut built_path = cwd.to_path_buf(); for part in &built.parts { built_path.push(part); } let Ok(result) = built_path.read_dir() else { return completions; }; let mut entries = Vec::new(); 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 { entries.push((entry_name, built)); } } let prefix = partial.first().unwrap_or(&""); let sorted_entries = sort_completions(prefix, entries, SortBy::Ascending, |(entry, _)| entry); for (entry_name, built) in sorted_entries { match partial.split_first() { Some((base, rest)) => { if matches(base, &entry_name, options) { // 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, cwd, options, want_directory, isdir, )); } else { completions.push(built); } } if entry_name.eq(base) && matches!(options.match_algorithm, MatchAlgorithm::Prefix) && isdir { break; } } None => { completions.push(built); } } } completions } #[derive(Debug)] enum OriginalCwd { None, Home, Prefix(String), } impl OriginalCwd { fn apply(&self, mut p: PathBuiltFromString) -> 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(MAIN_SEPARATOR_STR); if p.isdir { 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::>()[..] { [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, engine_state: &EngineState, stack: &Stack, ) -> Vec<(nu_protocol::Span, String, Option